158 changed files with 858 additions and 38017 deletions
@ -1,84 +1,84 @@ |
|||
from couchpotato.api import api_docs, api_docs_missing |
|||
from couchpotato.api import api_docs, api_docs_missing, api |
|||
from couchpotato.core.auth import requires_auth |
|||
from couchpotato.core.event import fireEvent |
|||
from couchpotato.core.helpers.request import getParams, jsonified |
|||
from couchpotato.core.helpers.variable import md5 |
|||
from couchpotato.core.logger import CPLog |
|||
from couchpotato.environment import Env |
|||
from flask.app import Flask |
|||
from flask.blueprints import Blueprint |
|||
from flask.globals import request |
|||
from flask.helpers import url_for |
|||
from flask.templating import render_template |
|||
from sqlalchemy.engine import create_engine |
|||
from sqlalchemy.orm import scoped_session |
|||
from sqlalchemy.orm.session import sessionmaker |
|||
from werkzeug.utils import redirect |
|||
from tornado import template |
|||
from tornado.web import RequestHandler |
|||
import os |
|||
import time |
|||
|
|||
log = CPLog(__name__) |
|||
|
|||
app = Flask(__name__, static_folder = 'nope') |
|||
web = Blueprint('web', __name__) |
|||
views = {} |
|||
template_loader = template.Loader(os.path.join(os.path.dirname(__file__), 'templates')) |
|||
|
|||
# Main web handler |
|||
@requires_auth |
|||
class WebHandler(RequestHandler): |
|||
def get(self, route): |
|||
if not views.get(route): |
|||
page_not_found(self) |
|||
return |
|||
self.write(views[route]()) |
|||
|
|||
def addView(route, func, static = False): |
|||
views[route] = func |
|||
|
|||
def get_session(engine = None): |
|||
return Env.getSession(engine) |
|||
|
|||
def addView(route, func, static = False): |
|||
web.add_url_rule(route + ('' if static else '/'), endpoint = route if route else 'index', view_func = func) |
|||
|
|||
""" Web view """ |
|||
@web.route('/') |
|||
@requires_auth |
|||
# Web view |
|||
def index(): |
|||
return render_template('index.html', sep = os.sep, fireEvent = fireEvent, env = Env) |
|||
return template_loader.load('index.html').generate(sep = os.sep, fireEvent = fireEvent, Env = Env) |
|||
addView('', index) |
|||
|
|||
""" Api view """ |
|||
@web.route('docs/') |
|||
@requires_auth |
|||
# API docs |
|||
def apiDocs(): |
|||
from couchpotato import app |
|||
routes = [] |
|||
for route, x in sorted(app.view_functions.iteritems()): |
|||
if route[0:4] == 'api.': |
|||
routes += [route[4:].replace('::', '.')] |
|||
|
|||
for route in api.iterkeys(): |
|||
routes.append(route) |
|||
|
|||
if api_docs.get(''): |
|||
del api_docs[''] |
|||
del api_docs_missing[''] |
|||
return render_template('api.html', fireEvent = fireEvent, routes = sorted(routes), api_docs = api_docs, api_docs_missing = sorted(api_docs_missing)) |
|||
|
|||
@web.route('getkey/') |
|||
def getApiKey(): |
|||
return template_loader.load('api.html').generate(fireEvent = fireEvent, routes = sorted(routes), api_docs = api_docs, api_docs_missing = sorted(api_docs_missing), Env = Env) |
|||
|
|||
api = None |
|||
params = getParams() |
|||
username = Env.setting('username') |
|||
password = Env.setting('password') |
|||
addView('docs', apiDocs) |
|||
|
|||
if (params.get('u') == md5(username) or not username) and (params.get('p') == password or not password): |
|||
api = Env.setting('api_key') |
|||
# Make non basic auth option to get api key |
|||
class KeyHandler(RequestHandler): |
|||
def get(self): |
|||
api = None |
|||
username = Env.setting('username') |
|||
password = Env.setting('password') |
|||
|
|||
return jsonified({ |
|||
'success': api is not None, |
|||
'api_key': api |
|||
}) |
|||
if (self.get_argument('u') == md5(username) or not username) and (self.get_argument('p') == password or not password): |
|||
api = Env.setting('api_key') |
|||
|
|||
@app.errorhandler(404) |
|||
def page_not_found(error): |
|||
index_url = url_for('web.index') |
|||
url = request.path[len(index_url):] |
|||
self.write({ |
|||
'success': api is not None, |
|||
'api_key': api |
|||
}) |
|||
|
|||
def page_not_found(rh): |
|||
index_url = Env.get('web_base') |
|||
url = rh.request.uri[len(index_url):] |
|||
|
|||
if url[:3] != 'api': |
|||
if request.path != '/': |
|||
r = request.url.replace(request.path, index_url + '#' + url) |
|||
else: |
|||
r = '%s%s' % (request.url.rstrip('/'), index_url + '#' + url) |
|||
return redirect(r) |
|||
r = index_url + '#' + url.lstrip('/') |
|||
rh.redirect(r) |
|||
else: |
|||
if not Env.get('dev'): |
|||
time.sleep(0.1) |
|||
return 'Wrong API key used', 404 |
|||
|
|||
rh.set_status(404) |
|||
rh.write('Wrong API key used') |
|||
|
|||
|
@ -1,26 +1,51 @@ |
|||
from couchpotato.core.helpers.variable import md5 |
|||
from couchpotato.environment import Env |
|||
from flask import request, Response |
|||
from functools import wraps |
|||
import base64 |
|||
|
|||
def check_auth(username, password): |
|||
return username == Env.setting('username') and password == Env.setting('password') |
|||
|
|||
def authenticate(): |
|||
return Response( |
|||
'This is not the page you are looking for. *waves hand*', 401, |
|||
{'WWW-Authenticate': 'Basic realm="CouchPotato Login"'} |
|||
) |
|||
def requires_auth(handler_class): |
|||
|
|||
def requires_auth(f): |
|||
def wrap_execute(handler_execute): |
|||
|
|||
@wraps(f) |
|||
def decorated(*args, **kwargs): |
|||
auth = getattr(request, 'authorization') |
|||
if Env.setting('username') and Env.setting('password'): |
|||
if (not auth or not check_auth(auth.username.decode('latin1'), md5(auth.password.decode('latin1').encode(Env.get('encoding'))))): |
|||
return authenticate() |
|||
def require_basic_auth(handler, kwargs): |
|||
if Env.setting('username') and Env.setting('password'): |
|||
|
|||
return f(*args, **kwargs) |
|||
auth_header = handler.request.headers.get('Authorization') |
|||
auth_decoded = base64.decodestring(auth_header[6:]) if auth_header else None |
|||
if auth_decoded: |
|||
username, password = auth_decoded.split(':', 2) |
|||
|
|||
return decorated |
|||
if auth_header is None or not auth_header.startswith('Basic ') or (not check_auth(username.decode('latin'), md5(password.decode('latin')))): |
|||
handler.set_status(401) |
|||
handler.set_header('WWW-Authenticate', 'Basic realm="CouchPotato Login"') |
|||
handler._transforms = [] |
|||
handler.finish() |
|||
|
|||
return False |
|||
|
|||
return True |
|||
|
|||
def _execute(self, transforms, *args, **kwargs): |
|||
|
|||
if not require_basic_auth(self, kwargs): |
|||
return False |
|||
return handler_execute(self, transforms, *args, **kwargs) |
|||
|
|||
return _execute |
|||
|
|||
handler_class._execute = wrap_execute(handler_class._execute) |
|||
|
|||
return handler_class |
|||
|
|||
# @wraps(f) |
|||
# def decorated(*args, **kwargs): |
|||
# auth = getattr(request, 'authorization') |
|||
# if Env.setting('username') and Env.setting('password'): |
|||
# if (not auth or not check_auth(auth.username.decode('latin1'), md5(auth.password.decode('latin1').encode(Env.get('encoding'))))): |
|||
# return authenticate() |
|||
# |
|||
# return f(*args, **kwargs) |
|||
# |
|||
# return decorated |
|||
|
@ -1,82 +0,0 @@ |
|||
from couchpotato.core.helpers.encoding import toUnicode |
|||
from couchpotato.core.helpers.variable import natcmp |
|||
from flask.globals import current_app |
|||
from flask.helpers import json, make_response |
|||
from urllib import unquote |
|||
from werkzeug.urls import url_decode |
|||
import flask |
|||
import re |
|||
|
|||
def getParams(): |
|||
|
|||
params = url_decode(getattr(flask.request, 'environ').get('QUERY_STRING', '')) |
|||
reg = re.compile('^[a-z0-9_\.]+$') |
|||
|
|||
current = temp = {} |
|||
for param, value in sorted(params.iteritems()): |
|||
|
|||
nest = re.split("([\[\]]+)", param) |
|||
if len(nest) > 1: |
|||
nested = [] |
|||
for key in nest: |
|||
if reg.match(key): |
|||
nested.append(key) |
|||
|
|||
current = temp |
|||
|
|||
for item in nested: |
|||
if item is nested[-1]: |
|||
current[item] = toUnicode(unquote(value)) |
|||
else: |
|||
try: |
|||
current[item] |
|||
except: |
|||
current[item] = {} |
|||
|
|||
current = current[item] |
|||
else: |
|||
temp[param] = toUnicode(unquote(value)) |
|||
|
|||
return dictToList(temp) |
|||
|
|||
def dictToList(params): |
|||
|
|||
if type(params) is dict: |
|||
new = {} |
|||
for x, value in params.iteritems(): |
|||
try: |
|||
new_value = [dictToList(value[k]) for k in sorted(value.iterkeys(), cmp = natcmp)] |
|||
except: |
|||
new_value = value |
|||
|
|||
new[x] = new_value |
|||
else: |
|||
new = params |
|||
|
|||
return new |
|||
|
|||
def getParam(attr, default = None): |
|||
try: |
|||
return getParams().get(attr, default) |
|||
except: |
|||
return default |
|||
|
|||
def padded_jsonify(callback, *args, **kwargs): |
|||
content = str(callback) + '(' + json.dumps(dict(*args, **kwargs)) + ')' |
|||
return getattr(current_app, 'response_class')(content, mimetype = 'text/javascript') |
|||
|
|||
def jsonify(mimetype, *args, **kwargs): |
|||
content = json.dumps(dict(*args, **kwargs)) |
|||
return getattr(current_app, 'response_class')(content, mimetype = mimetype) |
|||
|
|||
def jsonified(*args, **kwargs): |
|||
callback = getParam('callback_func', None) |
|||
if callback: |
|||
content = padded_jsonify(callback, *args, **kwargs) |
|||
else: |
|||
content = jsonify('application/json', *args, **kwargs) |
|||
|
|||
response = make_response(content) |
|||
response.cache_control.no_cache = True |
|||
|
|||
return response |
@ -1,6 +0,0 @@ |
|||
from .main import V1Importer |
|||
|
|||
def start(): |
|||
return V1Importer() |
|||
|
|||
config = [] |
@ -1,30 +0,0 @@ |
|||
<html> |
|||
<head> |
|||
<link rel="stylesheet" href="{{ url_for('web.static', filename='style/main.css') }}" type="text/css"> |
|||
<link rel="stylesheet" href="{{ url_for('web.static', filename='style/uniform.generic.css') }}" type="text/css"> |
|||
<link rel="stylesheet" href="{{ url_for('web.static', filename='style/uniform.css') }}" type="text/css"> |
|||
|
|||
<script type="text/javascript" src="{{ url_for('web.static', filename='scripts/library/mootools.js') }}"></script> |
|||
|
|||
<script type="text/javascript"> |
|||
|
|||
window.addEvent('domready', function(){ |
|||
if($('old_db')) |
|||
$('old_db').addEvent('change', function(){ |
|||
$('form').submit(); |
|||
}); |
|||
}); |
|||
|
|||
</script> |
|||
|
|||
</head> |
|||
<body> |
|||
{% if message: %} |
|||
{{ message }} |
|||
{% else: %} |
|||
<form id="form" method="post" enctype="multipart/form-data"> |
|||
<input type="file" name="old_db" id="old_db" /> |
|||
</form> |
|||
{% endif %} |
|||
</body> |
|||
</html> |
@ -1,56 +0,0 @@ |
|||
from couchpotato.api import addApiView |
|||
from couchpotato.core.event import fireEventAsync |
|||
from couchpotato.core.helpers.variable import getImdb |
|||
from couchpotato.core.logger import CPLog |
|||
from couchpotato.core.plugins.base import Plugin |
|||
from couchpotato.environment import Env |
|||
from flask.globals import request |
|||
from flask.helpers import url_for |
|||
import os |
|||
|
|||
log = CPLog(__name__) |
|||
|
|||
|
|||
class V1Importer(Plugin): |
|||
|
|||
def __init__(self): |
|||
addApiView('v1.import', self.fromOld, methods = ['GET', 'POST']) |
|||
|
|||
def fromOld(self): |
|||
|
|||
if request.method != 'POST': |
|||
return self.renderTemplate(__file__, 'form.html', url_for = url_for) |
|||
|
|||
file = request.files['old_db'] |
|||
|
|||
uploaded_file = os.path.join(Env.get('cache_dir'), 'v1_database.db') |
|||
|
|||
if os.path.isfile(uploaded_file): |
|||
os.remove(uploaded_file) |
|||
|
|||
file.save(uploaded_file) |
|||
|
|||
try: |
|||
import sqlite3 |
|||
conn = sqlite3.connect(uploaded_file) |
|||
|
|||
wanted = [] |
|||
|
|||
t = ('want',) |
|||
cur = conn.execute('SELECT status, imdb FROM Movie WHERE status=?', t) |
|||
for row in cur: |
|||
status, imdb = row |
|||
if getImdb(imdb): |
|||
wanted.append(imdb) |
|||
conn.close() |
|||
|
|||
wanted = set(wanted) |
|||
for imdb in wanted: |
|||
fireEventAsync('movie.add', {'identifier': imdb}, search_after = False) |
|||
|
|||
message = 'Successfully imported %s movie(s)' % len(wanted) |
|||
except Exception, e: |
|||
message = 'Failed: %s' % e |
|||
|
|||
return self.renderTemplate(__file__, 'form.html', url_for = url_for, message = message) |
|||
|
@ -0,0 +1,262 @@ |
|||
""" |
|||
copied from |
|||
werkzeug.contrib.cache |
|||
~~~~~~~~~~~~~~~~~~~~~~ |
|||
|
|||
:copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details. |
|||
:license: BSD, see LICENSE for more details. |
|||
""" |
|||
from cache.posixemulation import rename |
|||
from itertools import izip |
|||
from time import time |
|||
import os |
|||
import re |
|||
import tempfile |
|||
try: |
|||
from hashlib import md5 |
|||
except ImportError: |
|||
from md5 import new as md5 |
|||
|
|||
try: |
|||
import cPickle as pickle |
|||
except ImportError: |
|||
import pickle |
|||
|
|||
|
|||
def _items(mappingorseq): |
|||
"""Wrapper for efficient iteration over mappings represented by dicts |
|||
or sequences:: |
|||
|
|||
>>> for k, v in _items((i, i*i) for i in xrange(5)): |
|||
... assert k*k == v |
|||
|
|||
>>> for k, v in _items(dict((i, i*i) for i in xrange(5))): |
|||
... assert k*k == v |
|||
|
|||
""" |
|||
return mappingorseq.iteritems() if hasattr(mappingorseq, 'iteritems') \ |
|||
else mappingorseq |
|||
|
|||
|
|||
class BaseCache(object): |
|||
"""Baseclass for the cache systems. All the cache systems implement this |
|||
API or a superset of it. |
|||
|
|||
:param default_timeout: the default timeout that is used if no timeout is |
|||
specified on :meth:`set`. |
|||
""" |
|||
|
|||
def __init__(self, default_timeout = 300): |
|||
self.default_timeout = default_timeout |
|||
|
|||
def delete(self, key): |
|||
"""Deletes `key` from the cache. If it does not exist in the cache |
|||
nothing happens. |
|||
|
|||
:param key: the key to delete. |
|||
""" |
|||
pass |
|||
|
|||
def get_many(self, *keys): |
|||
"""Returns a list of values for the given keys. |
|||
For each key a item in the list is created. Example:: |
|||
|
|||
foo, bar = cache.get_many("foo", "bar") |
|||
|
|||
If a key can't be looked up `None` is returned for that key |
|||
instead. |
|||
|
|||
:param keys: The function accepts multiple keys as positional |
|||
arguments. |
|||
""" |
|||
return map(self.get, keys) |
|||
|
|||
def get_dict(self, *keys): |
|||
"""Works like :meth:`get_many` but returns a dict:: |
|||
|
|||
d = cache.get_dict("foo", "bar") |
|||
foo = d["foo"] |
|||
bar = d["bar"] |
|||
|
|||
:param keys: The function accepts multiple keys as positional |
|||
arguments. |
|||
""" |
|||
return dict(izip(keys, self.get_many(*keys))) |
|||
|
|||
def set(self, key, value, timeout = None): |
|||
"""Adds a new key/value to the cache (overwrites value, if key already |
|||
exists in the cache). |
|||
|
|||
:param key: the key to set |
|||
:param value: the value for the key |
|||
:param timeout: the cache timeout for the key (if not specified, |
|||
it uses the default timeout). |
|||
""" |
|||
pass |
|||
|
|||
def add(self, key, value, timeout = None): |
|||
"""Works like :meth:`set` but does not overwrite the values of already |
|||
existing keys. |
|||
|
|||
:param key: the key to set |
|||
:param value: the value for the key |
|||
:param timeout: the cache timeout for the key or the default |
|||
timeout if not specified. |
|||
""" |
|||
pass |
|||
|
|||
def set_many(self, mapping, timeout = None): |
|||
"""Sets multiple keys and values from a mapping. |
|||
|
|||
:param mapping: a mapping with the keys/values to set. |
|||
:param timeout: the cache timeout for the key (if not specified, |
|||
it uses the default timeout). |
|||
""" |
|||
for key, value in _items(mapping): |
|||
self.set(key, value, timeout) |
|||
|
|||
def delete_many(self, *keys): |
|||
"""Deletes multiple keys at once. |
|||
|
|||
:param keys: The function accepts multiple keys as positional |
|||
arguments. |
|||
""" |
|||
for key in keys: |
|||
self.delete(key) |
|||
|
|||
def clear(self): |
|||
"""Clears the cache. Keep in mind that not all caches support |
|||
completely clearing the cache. |
|||
""" |
|||
pass |
|||
|
|||
def inc(self, key, delta = 1): |
|||
"""Increments the value of a key by `delta`. If the key does |
|||
not yet exist it is initialized with `delta`. |
|||
|
|||
For supporting caches this is an atomic operation. |
|||
|
|||
:param key: the key to increment. |
|||
:param delta: the delta to add. |
|||
""" |
|||
self.set(key, (self.get(key) or 0) + delta) |
|||
|
|||
def dec(self, key, delta = 1): |
|||
"""Decrements the value of a key by `delta`. If the key does |
|||
not yet exist it is initialized with `-delta`. |
|||
|
|||
For supporting caches this is an atomic operation. |
|||
|
|||
:param key: the key to increment. |
|||
:param delta: the delta to subtract. |
|||
""" |
|||
self.set(key, (self.get(key) or 0) - delta) |
|||
|
|||
|
|||
class FileSystemCache(BaseCache): |
|||
"""A cache that stores the items on the file system. This cache depends |
|||
on being the only user of the `cache_dir`. Make absolutely sure that |
|||
nobody but this cache stores files there or otherwise the cache will |
|||
randomly delete files therein. |
|||
|
|||
:param cache_dir: the directory where cache files are stored. |
|||
:param threshold: the maximum number of items the cache stores before |
|||
it starts deleting some. |
|||
:param default_timeout: the default timeout that is used if no timeout is |
|||
specified on :meth:`~BaseCache.set`. |
|||
:param mode: the file mode wanted for the cache files, default 0600 |
|||
""" |
|||
|
|||
#: used for temporary files by the FileSystemCache |
|||
_fs_transaction_suffix = '.__wz_cache' |
|||
|
|||
def __init__(self, cache_dir, threshold = 500, default_timeout = 300, mode = 0600): |
|||
BaseCache.__init__(self, default_timeout) |
|||
self._path = cache_dir |
|||
self._threshold = threshold |
|||
self._mode = mode |
|||
if not os.path.exists(self._path): |
|||
os.makedirs(self._path) |
|||
|
|||
def _list_dir(self): |
|||
"""return a list of (fully qualified) cache filenames |
|||
""" |
|||
return [os.path.join(self._path, fn) for fn in os.listdir(self._path) |
|||
if not fn.endswith(self._fs_transaction_suffix)] |
|||
|
|||
def _prune(self): |
|||
entries = self._list_dir() |
|||
if len(entries) > self._threshold: |
|||
now = time() |
|||
for idx, fname in enumerate(entries): |
|||
remove = False |
|||
f = None |
|||
try: |
|||
try: |
|||
f = open(fname, 'rb') |
|||
expires = pickle.load(f) |
|||
remove = expires <= now or idx % 3 == 0 |
|||
finally: |
|||
if f is not None: |
|||
f.close() |
|||
except Exception: |
|||
pass |
|||
if remove: |
|||
try: |
|||
os.remove(fname) |
|||
except (IOError, OSError): |
|||
pass |
|||
|
|||
def clear(self): |
|||
for fname in self._list_dir(): |
|||
try: |
|||
os.remove(fname) |
|||
except (IOError, OSError): |
|||
pass |
|||
|
|||
def _get_filename(self, key): |
|||
hash = md5(key).hexdigest() |
|||
return os.path.join(self._path, hash) |
|||
|
|||
def get(self, key): |
|||
filename = self._get_filename(key) |
|||
try: |
|||
f = open(filename, 'rb') |
|||
try: |
|||
if pickle.load(f) >= time(): |
|||
return pickle.load(f) |
|||
finally: |
|||
f.close() |
|||
os.remove(filename) |
|||
except Exception: |
|||
return None |
|||
|
|||
def add(self, key, value, timeout = None): |
|||
filename = self._get_filename(key) |
|||
if not os.path.exists(filename): |
|||
self.set(key, value, timeout) |
|||
|
|||
def set(self, key, value, timeout = None): |
|||
if timeout is None: |
|||
timeout = self.default_timeout |
|||
filename = self._get_filename(key) |
|||
self._prune() |
|||
try: |
|||
fd, tmp = tempfile.mkstemp(suffix = self._fs_transaction_suffix, |
|||
dir = self._path) |
|||
f = os.fdopen(fd, 'wb') |
|||
try: |
|||
pickle.dump(int(time() + timeout), f, 1) |
|||
pickle.dump(value, f, pickle.HIGHEST_PROTOCOL) |
|||
finally: |
|||
f.close() |
|||
rename(tmp, filename) |
|||
os.chmod(filename, self._mode) |
|||
except (IOError, OSError): |
|||
pass |
|||
|
|||
def delete(self, key): |
|||
try: |
|||
os.remove(self._get_filename(key)) |
|||
except (IOError, OSError): |
|||
pass |
@ -1,44 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
flask |
|||
~~~~~ |
|||
|
|||
A microframework based on Werkzeug. It's extensively documented |
|||
and follows best practice patterns. |
|||
|
|||
:copyright: (c) 2011 by Armin Ronacher. |
|||
:license: BSD, see LICENSE for more details. |
|||
""" |
|||
|
|||
__version__ = '0.9' |
|||
|
|||
# utilities we import from Werkzeug and Jinja2 that are unused |
|||
# in the module but are exported as public interface. |
|||
from werkzeug.exceptions import abort |
|||
from werkzeug.utils import redirect |
|||
from jinja2 import Markup, escape |
|||
|
|||
from .app import Flask, Request, Response |
|||
from .config import Config |
|||
from .helpers import url_for, jsonify, json_available, flash, \ |
|||
send_file, send_from_directory, get_flashed_messages, \ |
|||
get_template_attribute, make_response, safe_join, \ |
|||
stream_with_context |
|||
from .globals import current_app, g, request, session, _request_ctx_stack, \ |
|||
_app_ctx_stack |
|||
from .ctx import has_request_context, has_app_context, \ |
|||
after_this_request |
|||
from .module import Module |
|||
from .blueprints import Blueprint |
|||
from .templating import render_template, render_template_string |
|||
|
|||
# the signals |
|||
from .signals import signals_available, template_rendered, request_started, \ |
|||
request_finished, got_request_exception, request_tearing_down |
|||
|
|||
# only import json if it's available |
|||
if json_available: |
|||
from .helpers import json |
|||
|
|||
# backwards compat, goes away in 1.0 |
|||
from .sessions import SecureCookieSession as Session |
File diff suppressed because it is too large
@ -1,345 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
flask.blueprints |
|||
~~~~~~~~~~~~~~~~ |
|||
|
|||
Blueprints are the recommended way to implement larger or more |
|||
pluggable applications in Flask 0.7 and later. |
|||
|
|||
:copyright: (c) 2011 by Armin Ronacher. |
|||
:license: BSD, see LICENSE for more details. |
|||
""" |
|||
from functools import update_wrapper |
|||
|
|||
from .helpers import _PackageBoundObject, _endpoint_from_view_func |
|||
|
|||
|
|||
class BlueprintSetupState(object): |
|||
"""Temporary holder object for registering a blueprint with the |
|||
application. An instance of this class is created by the |
|||
:meth:`~flask.Blueprint.make_setup_state` method and later passed |
|||
to all register callback functions. |
|||
""" |
|||
|
|||
def __init__(self, blueprint, app, options, first_registration): |
|||
#: a reference to the current application |
|||
self.app = app |
|||
|
|||
#: a reference to the blueprint that created this setup state. |
|||
self.blueprint = blueprint |
|||
|
|||
#: a dictionary with all options that were passed to the |
|||
#: :meth:`~flask.Flask.register_blueprint` method. |
|||
self.options = options |
|||
|
|||
#: as blueprints can be registered multiple times with the |
|||
#: application and not everything wants to be registered |
|||
#: multiple times on it, this attribute can be used to figure |
|||
#: out if the blueprint was registered in the past already. |
|||
self.first_registration = first_registration |
|||
|
|||
subdomain = self.options.get('subdomain') |
|||
if subdomain is None: |
|||
subdomain = self.blueprint.subdomain |
|||
|
|||
#: The subdomain that the blueprint should be active for, `None` |
|||
#: otherwise. |
|||
self.subdomain = subdomain |
|||
|
|||
url_prefix = self.options.get('url_prefix') |
|||
if url_prefix is None: |
|||
url_prefix = self.blueprint.url_prefix |
|||
|
|||
#: The prefix that should be used for all URLs defined on the |
|||
#: blueprint. |
|||
self.url_prefix = url_prefix |
|||
|
|||
#: A dictionary with URL defaults that is added to each and every |
|||
#: URL that was defined with the blueprint. |
|||
self.url_defaults = dict(self.blueprint.url_values_defaults) |
|||
self.url_defaults.update(self.options.get('url_defaults', ())) |
|||
|
|||
def add_url_rule(self, rule, endpoint=None, view_func=None, **options): |
|||
"""A helper method to register a rule (and optionally a view function) |
|||
to the application. The endpoint is automatically prefixed with the |
|||
blueprint's name. |
|||
""" |
|||
if self.url_prefix: |
|||
rule = self.url_prefix + rule |
|||
options.setdefault('subdomain', self.subdomain) |
|||
if endpoint is None: |
|||
endpoint = _endpoint_from_view_func(view_func) |
|||
defaults = self.url_defaults |
|||
if 'defaults' in options: |
|||
defaults = dict(defaults, **options.pop('defaults')) |
|||
self.app.add_url_rule(rule, '%s.%s' % (self.blueprint.name, endpoint), |
|||
view_func, defaults=defaults, **options) |
|||
|
|||
|
|||
class Blueprint(_PackageBoundObject): |
|||
"""Represents a blueprint. A blueprint is an object that records |
|||
functions that will be called with the |
|||
:class:`~flask.blueprint.BlueprintSetupState` later to register functions |
|||
or other things on the main application. See :ref:`blueprints` for more |
|||
information. |
|||
|
|||
.. versionadded:: 0.7 |
|||
""" |
|||
|
|||
warn_on_modifications = False |
|||
_got_registered_once = False |
|||
|
|||
def __init__(self, name, import_name, static_folder=None, |
|||
static_url_path=None, template_folder=None, |
|||
url_prefix=None, subdomain=None, url_defaults=None): |
|||
_PackageBoundObject.__init__(self, import_name, template_folder) |
|||
self.name = name |
|||
self.url_prefix = url_prefix |
|||
self.subdomain = subdomain |
|||
self.static_folder = static_folder |
|||
self.static_url_path = static_url_path |
|||
self.deferred_functions = [] |
|||
self.view_functions = {} |
|||
if url_defaults is None: |
|||
url_defaults = {} |
|||
self.url_values_defaults = url_defaults |
|||
|
|||
def record(self, func): |
|||
"""Registers a function that is called when the blueprint is |
|||
registered on the application. This function is called with the |
|||
state as argument as returned by the :meth:`make_setup_state` |
|||
method. |
|||
""" |
|||
if self._got_registered_once and self.warn_on_modifications: |
|||
from warnings import warn |
|||
warn(Warning('The blueprint was already registered once ' |
|||
'but is getting modified now. These changes ' |
|||
'will not show up.')) |
|||
self.deferred_functions.append(func) |
|||
|
|||
def record_once(self, func): |
|||
"""Works like :meth:`record` but wraps the function in another |
|||
function that will ensure the function is only called once. If the |
|||
blueprint is registered a second time on the application, the |
|||
function passed is not called. |
|||
""" |
|||
def wrapper(state): |
|||
if state.first_registration: |
|||
func(state) |
|||
return self.record(update_wrapper(wrapper, func)) |
|||
|
|||
def make_setup_state(self, app, options, first_registration=False): |
|||
"""Creates an instance of :meth:`~flask.blueprints.BlueprintSetupState` |
|||
object that is later passed to the register callback functions. |
|||
Subclasses can override this to return a subclass of the setup state. |
|||
""" |
|||
return BlueprintSetupState(self, app, options, first_registration) |
|||
|
|||
def register(self, app, options, first_registration=False): |
|||
"""Called by :meth:`Flask.register_blueprint` to register a blueprint |
|||
on the application. This can be overridden to customize the register |
|||
behavior. Keyword arguments from |
|||
:func:`~flask.Flask.register_blueprint` are directly forwarded to this |
|||
method in the `options` dictionary. |
|||
""" |
|||
self._got_registered_once = True |
|||
state = self.make_setup_state(app, options, first_registration) |
|||
if self.has_static_folder: |
|||
state.add_url_rule(self.static_url_path + '/<path:filename>', |
|||
view_func=self.send_static_file, |
|||
endpoint='static') |
|||
|
|||
for deferred in self.deferred_functions: |
|||
deferred(state) |
|||
|
|||
def route(self, rule, **options): |
|||
"""Like :meth:`Flask.route` but for a blueprint. The endpoint for the |
|||
:func:`url_for` function is prefixed with the name of the blueprint. |
|||
""" |
|||
def decorator(f): |
|||
endpoint = options.pop("endpoint", f.__name__) |
|||
self.add_url_rule(rule, endpoint, f, **options) |
|||
return f |
|||
return decorator |
|||
|
|||
def add_url_rule(self, rule, endpoint=None, view_func=None, **options): |
|||
"""Like :meth:`Flask.add_url_rule` but for a blueprint. The endpoint for |
|||
the :func:`url_for` function is prefixed with the name of the blueprint. |
|||
""" |
|||
if endpoint: |
|||
assert '.' not in endpoint, "Blueprint endpoint's should not contain dot's" |
|||
self.record(lambda s: |
|||
s.add_url_rule(rule, endpoint, view_func, **options)) |
|||
|
|||
def endpoint(self, endpoint): |
|||
"""Like :meth:`Flask.endpoint` but for a blueprint. This does not |
|||
prefix the endpoint with the blueprint name, this has to be done |
|||
explicitly by the user of this method. If the endpoint is prefixed |
|||
with a `.` it will be registered to the current blueprint, otherwise |
|||
it's an application independent endpoint. |
|||
""" |
|||
def decorator(f): |
|||
def register_endpoint(state): |
|||
state.app.view_functions[endpoint] = f |
|||
self.record_once(register_endpoint) |
|||
return f |
|||
return decorator |
|||
|
|||
def app_template_filter(self, name=None): |
|||
"""Register a custom template filter, available application wide. Like |
|||
:meth:`Flask.template_filter` but for a blueprint. |
|||
|
|||
:param name: the optional name of the filter, otherwise the |
|||
function name will be used. |
|||
""" |
|||
def decorator(f): |
|||
self.add_app_template_filter(f, name=name) |
|||
return f |
|||
return decorator |
|||
|
|||
def add_app_template_filter(self, f, name=None): |
|||
"""Register a custom template filter, available application wide. Like |
|||
:meth:`Flask.add_template_filter` but for a blueprint. Works exactly |
|||
like the :meth:`app_template_filter` decorator. |
|||
|
|||
:param name: the optional name of the filter, otherwise the |
|||
function name will be used. |
|||
""" |
|||
def register_template(state): |
|||
state.app.jinja_env.filters[name or f.__name__] = f |
|||
self.record_once(register_template) |
|||
|
|||
def before_request(self, f): |
|||
"""Like :meth:`Flask.before_request` but for a blueprint. This function |
|||
is only executed before each request that is handled by a function of |
|||
that blueprint. |
|||
""" |
|||
self.record_once(lambda s: s.app.before_request_funcs |
|||
.setdefault(self.name, []).append(f)) |
|||
return f |
|||
|
|||
def before_app_request(self, f): |
|||
"""Like :meth:`Flask.before_request`. Such a function is executed |
|||
before each request, even if outside of a blueprint. |
|||
""" |
|||
self.record_once(lambda s: s.app.before_request_funcs |
|||
.setdefault(None, []).append(f)) |
|||
return f |
|||
|
|||
def before_app_first_request(self, f): |
|||
"""Like :meth:`Flask.before_first_request`. Such a function is |
|||
executed before the first request to the application. |
|||
""" |
|||
self.record_once(lambda s: s.app.before_first_request_funcs.append(f)) |
|||
return f |
|||
|
|||
def after_request(self, f): |
|||
"""Like :meth:`Flask.after_request` but for a blueprint. This function |
|||
is only executed after each request that is handled by a function of |
|||
that blueprint. |
|||
""" |
|||
self.record_once(lambda s: s.app.after_request_funcs |
|||
.setdefault(self.name, []).append(f)) |
|||
return f |
|||
|
|||
def after_app_request(self, f): |
|||
"""Like :meth:`Flask.after_request` but for a blueprint. Such a function |
|||
is executed after each request, even if outside of the blueprint. |
|||
""" |
|||
self.record_once(lambda s: s.app.after_request_funcs |
|||
.setdefault(None, []).append(f)) |
|||
return f |
|||
|
|||
def teardown_request(self, f): |
|||
"""Like :meth:`Flask.teardown_request` but for a blueprint. This |
|||
function is only executed when tearing down requests handled by a |
|||
function of that blueprint. Teardown request functions are executed |
|||
when the request context is popped, even when no actual request was |
|||
performed. |
|||
""" |
|||
self.record_once(lambda s: s.app.teardown_request_funcs |
|||
.setdefault(self.name, []).append(f)) |
|||
return f |
|||
|
|||
def teardown_app_request(self, f): |
|||
"""Like :meth:`Flask.teardown_request` but for a blueprint. Such a |
|||
function is executed when tearing down each request, even if outside of |
|||
the blueprint. |
|||
""" |
|||
self.record_once(lambda s: s.app.teardown_request_funcs |
|||
.setdefault(None, []).append(f)) |
|||
return f |
|||
|
|||
def context_processor(self, f): |
|||
"""Like :meth:`Flask.context_processor` but for a blueprint. This |
|||
function is only executed for requests handled by a blueprint. |
|||
""" |
|||
self.record_once(lambda s: s.app.template_context_processors |
|||
.setdefault(self.name, []).append(f)) |
|||
return f |
|||
|
|||
def app_context_processor(self, f): |
|||
"""Like :meth:`Flask.context_processor` but for a blueprint. Such a |
|||
function is executed each request, even if outside of the blueprint. |
|||
""" |
|||
self.record_once(lambda s: s.app.template_context_processors |
|||
.setdefault(None, []).append(f)) |
|||
return f |
|||
|
|||
def app_errorhandler(self, code): |
|||
"""Like :meth:`Flask.errorhandler` but for a blueprint. This |
|||
handler is used for all requests, even if outside of the blueprint. |
|||
""" |
|||
def decorator(f): |
|||
self.record_once(lambda s: s.app.errorhandler(code)(f)) |
|||
return f |
|||
return decorator |
|||
|
|||
def url_value_preprocessor(self, f): |
|||
"""Registers a function as URL value preprocessor for this |
|||
blueprint. It's called before the view functions are called and |
|||
can modify the url values provided. |
|||
""" |
|||
self.record_once(lambda s: s.app.url_value_preprocessors |
|||
.setdefault(self.name, []).append(f)) |
|||
return f |
|||
|
|||
def url_defaults(self, f): |
|||
"""Callback function for URL defaults for this blueprint. It's called |
|||
with the endpoint and values and should update the values passed |
|||
in place. |
|||
""" |
|||
self.record_once(lambda s: s.app.url_default_functions |
|||
.setdefault(self.name, []).append(f)) |
|||
return f |
|||
|
|||
def app_url_value_preprocessor(self, f): |
|||
"""Same as :meth:`url_value_preprocessor` but application wide. |
|||
""" |
|||
self.record_once(lambda s: s.app.url_value_preprocessors |
|||
.setdefault(None, []).append(f)) |
|||
return f |
|||
|
|||
def app_url_defaults(self, f): |
|||
"""Same as :meth:`url_defaults` but application wide. |
|||
""" |
|||
self.record_once(lambda s: s.app.url_default_functions |
|||
.setdefault(None, []).append(f)) |
|||
return f |
|||
|
|||
def errorhandler(self, code_or_exception): |
|||
"""Registers an error handler that becomes active for this blueprint |
|||
only. Please be aware that routing does not happen local to a |
|||
blueprint so an error handler for 404 usually is not handled by |
|||
a blueprint unless it is caused inside a view function. Another |
|||
special case is the 500 internal server error which is always looked |
|||
up from the application. |
|||
|
|||
Otherwise works as the :meth:`~flask.Flask.errorhandler` decorator |
|||
of the :class:`~flask.Flask` object. |
|||
""" |
|||
def decorator(f): |
|||
self.record_once(lambda s: s.app._register_error_handler( |
|||
self.name, code_or_exception, f)) |
|||
return f |
|||
return decorator |
@ -1,168 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
flask.config |
|||
~~~~~~~~~~~~ |
|||
|
|||
Implements the configuration related objects. |
|||
|
|||
:copyright: (c) 2011 by Armin Ronacher. |
|||
:license: BSD, see LICENSE for more details. |
|||
""" |
|||
|
|||
from __future__ import with_statement |
|||
|
|||
import imp |
|||
import os |
|||
import errno |
|||
|
|||
from werkzeug.utils import import_string |
|||
|
|||
|
|||
class ConfigAttribute(object): |
|||
"""Makes an attribute forward to the config""" |
|||
|
|||
def __init__(self, name, get_converter=None): |
|||
self.__name__ = name |
|||
self.get_converter = get_converter |
|||
|
|||
def __get__(self, obj, type=None): |
|||
if obj is None: |
|||
return self |
|||
rv = obj.config[self.__name__] |
|||
if self.get_converter is not None: |
|||
rv = self.get_converter(rv) |
|||
return rv |
|||
|
|||
def __set__(self, obj, value): |
|||
obj.config[self.__name__] = value |
|||
|
|||
|
|||
class Config(dict): |
|||
"""Works exactly like a dict but provides ways to fill it from files |
|||
or special dictionaries. There are two common patterns to populate the |
|||
config. |
|||
|
|||
Either you can fill the config from a config file:: |
|||
|
|||
app.config.from_pyfile('yourconfig.cfg') |
|||
|
|||
Or alternatively you can define the configuration options in the |
|||
module that calls :meth:`from_object` or provide an import path to |
|||
a module that should be loaded. It is also possible to tell it to |
|||
use the same module and with that provide the configuration values |
|||
just before the call:: |
|||
|
|||
DEBUG = True |
|||
SECRET_KEY = 'development key' |
|||
app.config.from_object(__name__) |
|||
|
|||
In both cases (loading from any Python file or loading from modules), |
|||
only uppercase keys are added to the config. This makes it possible to use |
|||
lowercase values in the config file for temporary values that are not added |
|||
to the config or to define the config keys in the same file that implements |
|||
the application. |
|||
|
|||
Probably the most interesting way to load configurations is from an |
|||
environment variable pointing to a file:: |
|||
|
|||
app.config.from_envvar('YOURAPPLICATION_SETTINGS') |
|||
|
|||
In this case before launching the application you have to set this |
|||
environment variable to the file you want to use. On Linux and OS X |
|||
use the export statement:: |
|||
|
|||
export YOURAPPLICATION_SETTINGS='/path/to/config/file' |
|||
|
|||
On windows use `set` instead. |
|||
|
|||
:param root_path: path to which files are read relative from. When the |
|||
config object is created by the application, this is |
|||
the application's :attr:`~flask.Flask.root_path`. |
|||
:param defaults: an optional dictionary of default values |
|||
""" |
|||
|
|||
def __init__(self, root_path, defaults=None): |
|||
dict.__init__(self, defaults or {}) |
|||
self.root_path = root_path |
|||
|
|||
def from_envvar(self, variable_name, silent=False): |
|||
"""Loads a configuration from an environment variable pointing to |
|||
a configuration file. This is basically just a shortcut with nicer |
|||
error messages for this line of code:: |
|||
|
|||
app.config.from_pyfile(os.environ['YOURAPPLICATION_SETTINGS']) |
|||
|
|||
:param variable_name: name of the environment variable |
|||
:param silent: set to `True` if you want silent failure for missing |
|||
files. |
|||
:return: bool. `True` if able to load config, `False` otherwise. |
|||
""" |
|||
rv = os.environ.get(variable_name) |
|||
if not rv: |
|||
if silent: |
|||
return False |
|||
raise RuntimeError('The environment variable %r is not set ' |
|||
'and as such configuration could not be ' |
|||
'loaded. Set this variable and make it ' |
|||
'point to a configuration file' % |
|||
variable_name) |
|||
return self.from_pyfile(rv, silent=silent) |
|||
|
|||
def from_pyfile(self, filename, silent=False): |
|||
"""Updates the values in the config from a Python file. This function |
|||
behaves as if the file was imported as module with the |
|||
:meth:`from_object` function. |
|||
|
|||
:param filename: the filename of the config. This can either be an |
|||
absolute filename or a filename relative to the |
|||
root path. |
|||
:param silent: set to `True` if you want silent failure for missing |
|||
files. |
|||
|
|||
.. versionadded:: 0.7 |
|||
`silent` parameter. |
|||
""" |
|||
filename = os.path.join(self.root_path, filename) |
|||
d = imp.new_module('config') |
|||
d.__file__ = filename |
|||
try: |
|||
execfile(filename, d.__dict__) |
|||
except IOError, e: |
|||
if silent and e.errno in (errno.ENOENT, errno.EISDIR): |
|||
return False |
|||
e.strerror = 'Unable to load configuration file (%s)' % e.strerror |
|||
raise |
|||
self.from_object(d) |
|||
return True |
|||
|
|||
def from_object(self, obj): |
|||
"""Updates the values from the given object. An object can be of one |
|||
of the following two types: |
|||
|
|||
- a string: in this case the object with that name will be imported |
|||
- an actual object reference: that object is used directly |
|||
|
|||
Objects are usually either modules or classes. |
|||
|
|||
Just the uppercase variables in that object are stored in the config. |
|||
Example usage:: |
|||
|
|||
app.config.from_object('yourapplication.default_config') |
|||
from yourapplication import default_config |
|||
app.config.from_object(default_config) |
|||
|
|||
You should not use this function to load the actual configuration but |
|||
rather configuration defaults. The actual config should be loaded |
|||
with :meth:`from_pyfile` and ideally from a location not within the |
|||
package because the package might be installed system wide. |
|||
|
|||
:param obj: an import name or object |
|||
""" |
|||
if isinstance(obj, basestring): |
|||
obj = import_string(obj) |
|||
for key in dir(obj): |
|||
if key.isupper(): |
|||
self[key] = getattr(obj, key) |
|||
|
|||
def __repr__(self): |
|||
return '<%s %s>' % (self.__class__.__name__, dict.__repr__(self)) |
@ -1,295 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
flask.ctx |
|||
~~~~~~~~~ |
|||
|
|||
Implements the objects required to keep the context. |
|||
|
|||
:copyright: (c) 2011 by Armin Ronacher. |
|||
:license: BSD, see LICENSE for more details. |
|||
""" |
|||
|
|||
import sys |
|||
|
|||
from werkzeug.exceptions import HTTPException |
|||
|
|||
from .globals import _request_ctx_stack, _app_ctx_stack |
|||
from .module import blueprint_is_module |
|||
|
|||
|
|||
class _RequestGlobals(object): |
|||
"""A plain object.""" |
|||
pass |
|||
|
|||
|
|||
def after_this_request(f): |
|||
"""Executes a function after this request. This is useful to modify |
|||
response objects. The function is passed the response object and has |
|||
to return the same or a new one. |
|||
|
|||
Example:: |
|||
|
|||
@app.route('/') |
|||
def index(): |
|||
@after_this_request |
|||
def add_header(response): |
|||
response.headers['X-Foo'] = 'Parachute' |
|||
return response |
|||
return 'Hello World!' |
|||
|
|||
This is more useful if a function other than the view function wants to |
|||
modify a response. For instance think of a decorator that wants to add |
|||
some headers without converting the return value into a response object. |
|||
|
|||
.. versionadded:: 0.9 |
|||
""" |
|||
_request_ctx_stack.top._after_request_functions.append(f) |
|||
return f |
|||
|
|||
|
|||
def has_request_context(): |
|||
"""If you have code that wants to test if a request context is there or |
|||
not this function can be used. For instance, you may want to take advantage |
|||
of request information if the request object is available, but fail |
|||
silently if it is unavailable. |
|||
|
|||
:: |
|||
|
|||
class User(db.Model): |
|||
|
|||
def __init__(self, username, remote_addr=None): |
|||
self.username = username |
|||
if remote_addr is None and has_request_context(): |
|||
remote_addr = request.remote_addr |
|||
self.remote_addr = remote_addr |
|||
|
|||
Alternatively you can also just test any of the context bound objects |
|||
(such as :class:`request` or :class:`g` for truthness):: |
|||
|
|||
class User(db.Model): |
|||
|
|||
def __init__(self, username, remote_addr=None): |
|||
self.username = username |
|||
if remote_addr is None and request: |
|||
remote_addr = request.remote_addr |
|||
self.remote_addr = remote_addr |
|||
|
|||
.. versionadded:: 0.7 |
|||
""" |
|||
return _request_ctx_stack.top is not None |
|||
|
|||
|
|||
def has_app_context(): |
|||
"""Works like :func:`has_request_context` but for the application |
|||
context. You can also just do a boolean check on the |
|||
:data:`current_app` object instead. |
|||
|
|||
.. versionadded:: 0.9 |
|||
""" |
|||
return _app_ctx_stack.top is not None |
|||
|
|||
|
|||
class AppContext(object): |
|||
"""The application context binds an application object implicitly |
|||
to the current thread or greenlet, similar to how the |
|||
:class:`RequestContext` binds request information. The application |
|||
context is also implicitly created if a request context is created |
|||
but the application is not on top of the individual application |
|||
context. |
|||
""" |
|||
|
|||
def __init__(self, app): |
|||
self.app = app |
|||
self.url_adapter = app.create_url_adapter(None) |
|||
|
|||
# Like request context, app contexts can be pushed multiple times |
|||
# but there a basic "refcount" is enough to track them. |
|||
self._refcnt = 0 |
|||
|
|||
def push(self): |
|||
"""Binds the app context to the current context.""" |
|||
self._refcnt += 1 |
|||
_app_ctx_stack.push(self) |
|||
|
|||
def pop(self, exc=None): |
|||
"""Pops the app context.""" |
|||
self._refcnt -= 1 |
|||
if self._refcnt <= 0: |
|||
if exc is None: |
|||
exc = sys.exc_info()[1] |
|||
self.app.do_teardown_appcontext(exc) |
|||
rv = _app_ctx_stack.pop() |
|||
assert rv is self, 'Popped wrong app context. (%r instead of %r)' \ |
|||
% (rv, self) |
|||
|
|||
def __enter__(self): |
|||
self.push() |
|||
return self |
|||
|
|||
def __exit__(self, exc_type, exc_value, tb): |
|||
self.pop(exc_value) |
|||
|
|||
|
|||
class RequestContext(object): |
|||
"""The request context contains all request relevant information. It is |
|||
created at the beginning of the request and pushed to the |
|||
`_request_ctx_stack` and removed at the end of it. It will create the |
|||
URL adapter and request object for the WSGI environment provided. |
|||
|
|||
Do not attempt to use this class directly, instead use |
|||
:meth:`~flask.Flask.test_request_context` and |
|||
:meth:`~flask.Flask.request_context` to create this object. |
|||
|
|||
When the request context is popped, it will evaluate all the |
|||
functions registered on the application for teardown execution |
|||
(:meth:`~flask.Flask.teardown_request`). |
|||
|
|||
The request context is automatically popped at the end of the request |
|||
for you. In debug mode the request context is kept around if |
|||
exceptions happen so that interactive debuggers have a chance to |
|||
introspect the data. With 0.4 this can also be forced for requests |
|||
that did not fail and outside of `DEBUG` mode. By setting |
|||
``'flask._preserve_context'`` to `True` on the WSGI environment the |
|||
context will not pop itself at the end of the request. This is used by |
|||
the :meth:`~flask.Flask.test_client` for example to implement the |
|||
deferred cleanup functionality. |
|||
|
|||
You might find this helpful for unittests where you need the |
|||
information from the context local around for a little longer. Make |
|||
sure to properly :meth:`~werkzeug.LocalStack.pop` the stack yourself in |
|||
that situation, otherwise your unittests will leak memory. |
|||
""" |
|||
|
|||
def __init__(self, app, environ): |
|||
self.app = app |
|||
self.request = app.request_class(environ) |
|||
self.url_adapter = app.create_url_adapter(self.request) |
|||
self.g = app.request_globals_class() |
|||
self.flashes = None |
|||
self.session = None |
|||
|
|||
# Request contexts can be pushed multiple times and interleaved with |
|||
# other request contexts. Now only if the last level is popped we |
|||
# get rid of them. Additionally if an application context is missing |
|||
# one is created implicitly so for each level we add this information |
|||
self._implicit_app_ctx_stack = [] |
|||
|
|||
# indicator if the context was preserved. Next time another context |
|||
# is pushed the preserved context is popped. |
|||
self.preserved = False |
|||
|
|||
# Functions that should be executed after the request on the response |
|||
# object. These will be called before the regular "after_request" |
|||
# functions. |
|||
self._after_request_functions = [] |
|||
|
|||
self.match_request() |
|||
|
|||
# XXX: Support for deprecated functionality. This is going away with |
|||
# Flask 1.0 |
|||
blueprint = self.request.blueprint |
|||
if blueprint is not None: |
|||
# better safe than sorry, we don't want to break code that |
|||
# already worked |
|||
bp = app.blueprints.get(blueprint) |
|||
if bp is not None and blueprint_is_module(bp): |
|||
self.request._is_old_module = True |
|||
|
|||
def match_request(self): |
|||
"""Can be overridden by a subclass to hook into the matching |
|||
of the request. |
|||
""" |
|||
try: |
|||
url_rule, self.request.view_args = \ |
|||
self.url_adapter.match(return_rule=True) |
|||
self.request.url_rule = url_rule |
|||
except HTTPException, e: |
|||
self.request.routing_exception = e |
|||
|
|||
def push(self): |
|||
"""Binds the request context to the current context.""" |
|||
# If an exception ocurrs in debug mode or if context preservation is |
|||
# activated under exception situations exactly one context stays |
|||
# on the stack. The rationale is that you want to access that |
|||
# information under debug situations. However if someone forgets to |
|||
# pop that context again we want to make sure that on the next push |
|||
# it's invalidated otherwise we run at risk that something leaks |
|||
# memory. This is usually only a problem in testsuite since this |
|||
# functionality is not active in production environments. |
|||
top = _request_ctx_stack.top |
|||
if top is not None and top.preserved: |
|||
top.pop() |
|||
|
|||
# Before we push the request context we have to ensure that there |
|||
# is an application context. |
|||
app_ctx = _app_ctx_stack.top |
|||
if app_ctx is None or app_ctx.app != self.app: |
|||
app_ctx = self.app.app_context() |
|||
app_ctx.push() |
|||
self._implicit_app_ctx_stack.append(app_ctx) |
|||
else: |
|||
self._implicit_app_ctx_stack.append(None) |
|||
|
|||
_request_ctx_stack.push(self) |
|||
|
|||
# Open the session at the moment that the request context is |
|||
# available. This allows a custom open_session method to use the |
|||
# request context (e.g. flask-sqlalchemy). |
|||
self.session = self.app.open_session(self.request) |
|||
if self.session is None: |
|||
self.session = self.app.make_null_session() |
|||
|
|||
def pop(self, exc=None): |
|||
"""Pops the request context and unbinds it by doing that. This will |
|||
also trigger the execution of functions registered by the |
|||
:meth:`~flask.Flask.teardown_request` decorator. |
|||
|
|||
.. versionchanged:: 0.9 |
|||
Added the `exc` argument. |
|||
""" |
|||
app_ctx = self._implicit_app_ctx_stack.pop() |
|||
|
|||
clear_request = False |
|||
if not self._implicit_app_ctx_stack: |
|||
self.preserved = False |
|||
if exc is None: |
|||
exc = sys.exc_info()[1] |
|||
self.app.do_teardown_request(exc) |
|||
clear_request = True |
|||
|
|||
rv = _request_ctx_stack.pop() |
|||
assert rv is self, 'Popped wrong request context. (%r instead of %r)' \ |
|||
% (rv, self) |
|||
|
|||
# get rid of circular dependencies at the end of the request |
|||
# so that we don't require the GC to be active. |
|||
if clear_request: |
|||
rv.request.environ['werkzeug.request'] = None |
|||
|
|||
# Get rid of the app as well if necessary. |
|||
if app_ctx is not None: |
|||
app_ctx.pop(exc) |
|||
|
|||
def __enter__(self): |
|||
self.push() |
|||
return self |
|||
|
|||
def __exit__(self, exc_type, exc_value, tb): |
|||
# do not pop the request stack if we are in debug mode and an |
|||
# exception happened. This will allow the debugger to still |
|||
# access the request object in the interactive shell. Furthermore |
|||
# the context can be force kept alive for the test client. |
|||
# See flask.testing for how this works. |
|||
if self.request.environ.get('flask._preserve_context') or \ |
|||
(tb is not None and self.app.preserve_context_on_exception): |
|||
self.preserved = True |
|||
else: |
|||
self.pop(exc_value) |
|||
|
|||
def __repr__(self): |
|||
return '<%s \'%s\' [%s] of %s>' % ( |
|||
self.__class__.__name__, |
|||
self.request.url, |
|||
self.request.method, |
|||
self.app.name |
|||
) |
@ -1,79 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
flask.debughelpers |
|||
~~~~~~~~~~~~~~~~~~ |
|||
|
|||
Various helpers to make the development experience better. |
|||
|
|||
:copyright: (c) 2011 by Armin Ronacher. |
|||
:license: BSD, see LICENSE for more details. |
|||
""" |
|||
|
|||
|
|||
class DebugFilesKeyError(KeyError, AssertionError): |
|||
"""Raised from request.files during debugging. The idea is that it can |
|||
provide a better error message than just a generic KeyError/BadRequest. |
|||
""" |
|||
|
|||
def __init__(self, request, key): |
|||
form_matches = request.form.getlist(key) |
|||
buf = ['You tried to access the file "%s" in the request.files ' |
|||
'dictionary but it does not exist. The mimetype for the request ' |
|||
'is "%s" instead of "multipart/form-data" which means that no ' |
|||
'file contents were transmitted. To fix this error you should ' |
|||
'provide enctype="multipart/form-data" in your form.' % |
|||
(key, request.mimetype)] |
|||
if form_matches: |
|||
buf.append('\n\nThe browser instead transmitted some file names. ' |
|||
'This was submitted: %s' % ', '.join('"%s"' % x |
|||
for x in form_matches)) |
|||
self.msg = ''.join(buf).encode('utf-8') |
|||
|
|||
def __str__(self): |
|||
return self.msg |
|||
|
|||
|
|||
class FormDataRoutingRedirect(AssertionError): |
|||
"""This exception is raised by Flask in debug mode if it detects a |
|||
redirect caused by the routing system when the request method is not |
|||
GET, HEAD or OPTIONS. Reasoning: form data will be dropped. |
|||
""" |
|||
|
|||
def __init__(self, request): |
|||
exc = request.routing_exception |
|||
buf = ['A request was sent to this URL (%s) but a redirect was ' |
|||
'issued automatically by the routing system to "%s".' |
|||
% (request.url, exc.new_url)] |
|||
|
|||
# In case just a slash was appended we can be extra helpful |
|||
if request.base_url + '/' == exc.new_url.split('?')[0]: |
|||
buf.append(' The URL was defined with a trailing slash so ' |
|||
'Flask will automatically redirect to the URL ' |
|||
'with the trailing slash if it was accessed ' |
|||
'without one.') |
|||
|
|||
buf.append(' Make sure to directly send your %s-request to this URL ' |
|||
'since we can\'t make browsers or HTTP clients redirect ' |
|||
'with form data reliably or without user interaction.' % |
|||
request.method) |
|||
buf.append('\n\nNote: this exception is only raised in debug mode') |
|||
AssertionError.__init__(self, ''.join(buf).encode('utf-8')) |
|||
|
|||
|
|||
def attach_enctype_error_multidict(request): |
|||
"""Since Flask 0.8 we're monkeypatching the files object in case a |
|||
request is detected that does not use multipart form data but the files |
|||
object is accessed. |
|||
""" |
|||
oldcls = request.files.__class__ |
|||
class newcls(oldcls): |
|||
def __getitem__(self, key): |
|||
try: |
|||
return oldcls.__getitem__(self, key) |
|||
except KeyError, e: |
|||
if key not in request.form: |
|||
raise |
|||
raise DebugFilesKeyError(request, key) |
|||
newcls.__name__ = oldcls.__name__ |
|||
newcls.__module__ = oldcls.__module__ |
|||
request.files.__class__ = newcls |
@ -1,49 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
flask.exceptions |
|||
~~~~~~~~~~~~ |
|||
|
|||
Flask specific additions to :class:`~werkzeug.exceptions.HTTPException` |
|||
|
|||
:copyright: (c) 2011 by Armin Ronacher. |
|||
:license: BSD, see LICENSE for more details. |
|||
""" |
|||
from werkzeug.exceptions import HTTPException, BadRequest |
|||
from .helpers import json |
|||
|
|||
|
|||
class JSONHTTPException(HTTPException): |
|||
"""A base class for HTTP exceptions with ``Content-Type: |
|||
application/json``. |
|||
|
|||
The ``description`` attribute of this class must set to a string (*not* an |
|||
HTML string) which describes the error. |
|||
|
|||
""" |
|||
|
|||
def get_body(self, environ): |
|||
"""Overrides :meth:`werkzeug.exceptions.HTTPException.get_body` to |
|||
return the description of this error in JSON format instead of HTML. |
|||
|
|||
""" |
|||
return json.dumps(dict(description=self.get_description(environ))) |
|||
|
|||
def get_headers(self, environ): |
|||
"""Returns a list of headers including ``Content-Type: |
|||
application/json``. |
|||
|
|||
""" |
|||
return [('Content-Type', 'application/json')] |
|||
|
|||
|
|||
class JSONBadRequest(JSONHTTPException, BadRequest): |
|||
"""Represents an HTTP ``400 Bad Request`` error whose body contains an |
|||
error message in JSON format instead of HTML format (as in the superclass). |
|||
|
|||
""" |
|||
|
|||
#: The description of the error which occurred as a string. |
|||
description = ( |
|||
'The browser (or proxy) sent a request that this server could not ' |
|||
'understand.' |
|||
) |
@ -1,29 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
flask.ext |
|||
~~~~~~~~~ |
|||
|
|||
Redirect imports for extensions. This module basically makes it possible |
|||
for us to transition from flaskext.foo to flask_foo without having to |
|||
force all extensions to upgrade at the same time. |
|||
|
|||
When a user does ``from flask.ext.foo import bar`` it will attempt to |
|||
import ``from flask_foo import bar`` first and when that fails it will |
|||
try to import ``from flaskext.foo import bar``. |
|||
|
|||
We're switching from namespace packages because it was just too painful for |
|||
everybody involved. |
|||
|
|||
:copyright: (c) 2011 by Armin Ronacher. |
|||
:license: BSD, see LICENSE for more details. |
|||
""" |
|||
|
|||
|
|||
def setup(): |
|||
from ..exthook import ExtensionImporter |
|||
importer = ExtensionImporter(['flask_%s', 'flaskext.%s'], __name__) |
|||
importer.install() |
|||
|
|||
|
|||
setup() |
|||
del setup |
@ -1,119 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
flask.exthook |
|||
~~~~~~~~~~~~~ |
|||
|
|||
Redirect imports for extensions. This module basically makes it possible |
|||
for us to transition from flaskext.foo to flask_foo without having to |
|||
force all extensions to upgrade at the same time. |
|||
|
|||
When a user does ``from flask.ext.foo import bar`` it will attempt to |
|||
import ``from flask_foo import bar`` first and when that fails it will |
|||
try to import ``from flaskext.foo import bar``. |
|||
|
|||
We're switching from namespace packages because it was just too painful for |
|||
everybody involved. |
|||
|
|||
This is used by `flask.ext`. |
|||
|
|||
:copyright: (c) 2011 by Armin Ronacher. |
|||
:license: BSD, see LICENSE for more details. |
|||
""" |
|||
import sys |
|||
import os |
|||
|
|||
|
|||
class ExtensionImporter(object): |
|||
"""This importer redirects imports from this submodule to other locations. |
|||
This makes it possible to transition from the old flaskext.name to the |
|||
newer flask_name without people having a hard time. |
|||
""" |
|||
|
|||
def __init__(self, module_choices, wrapper_module): |
|||
self.module_choices = module_choices |
|||
self.wrapper_module = wrapper_module |
|||
self.prefix = wrapper_module + '.' |
|||
self.prefix_cutoff = wrapper_module.count('.') + 1 |
|||
|
|||
def __eq__(self, other): |
|||
return self.__class__.__module__ == other.__class__.__module__ and \ |
|||
self.__class__.__name__ == other.__class__.__name__ and \ |
|||
self.wrapper_module == other.wrapper_module and \ |
|||
self.module_choices == other.module_choices |
|||
|
|||
def __ne__(self, other): |
|||
return not self.__eq__(other) |
|||
|
|||
def install(self): |
|||
sys.meta_path[:] = [x for x in sys.meta_path if self != x] + [self] |
|||
|
|||
def find_module(self, fullname, path=None): |
|||
if fullname.startswith(self.prefix): |
|||
return self |
|||
|
|||
def load_module(self, fullname): |
|||
if fullname in sys.modules: |
|||
return sys.modules[fullname] |
|||
modname = fullname.split('.', self.prefix_cutoff)[self.prefix_cutoff] |
|||
for path in self.module_choices: |
|||
realname = path % modname |
|||
try: |
|||
__import__(realname) |
|||
except ImportError: |
|||
exc_type, exc_value, tb = sys.exc_info() |
|||
# since we only establish the entry in sys.modules at the |
|||
# very this seems to be redundant, but if recursive imports |
|||
# happen we will call into the move import a second time. |
|||
# On the second invocation we still don't have an entry for |
|||
# fullname in sys.modules, but we will end up with the same |
|||
# fake module name and that import will succeed since this |
|||
# one already has a temporary entry in the modules dict. |
|||
# Since this one "succeeded" temporarily that second |
|||
# invocation now will have created a fullname entry in |
|||
# sys.modules which we have to kill. |
|||
sys.modules.pop(fullname, None) |
|||
|
|||
# If it's an important traceback we reraise it, otherwise |
|||
# we swallow it and try the next choice. The skipped frame |
|||
# is the one from __import__ above which we don't care about |
|||
if self.is_important_traceback(realname, tb): |
|||
raise exc_type, exc_value, tb.tb_next |
|||
continue |
|||
module = sys.modules[fullname] = sys.modules[realname] |
|||
if '.' not in modname: |
|||
setattr(sys.modules[self.wrapper_module], modname, module) |
|||
return module |
|||
raise ImportError('No module named %s' % fullname) |
|||
|
|||
def is_important_traceback(self, important_module, tb): |
|||
"""Walks a traceback's frames and checks if any of the frames |
|||
originated in the given important module. If that is the case then we |
|||
were able to import the module itself but apparently something went |
|||
wrong when the module was imported. (Eg: import of an import failed). |
|||
""" |
|||
while tb is not None: |
|||
if self.is_important_frame(important_module, tb): |
|||
return True |
|||
tb = tb.tb_next |
|||
return False |
|||
|
|||
def is_important_frame(self, important_module, tb): |
|||
"""Checks a single frame if it's important.""" |
|||
g = tb.tb_frame.f_globals |
|||
if '__name__' not in g: |
|||
return False |
|||
|
|||
module_name = g['__name__'] |
|||
|
|||
# Python 2.7 Behavior. Modules are cleaned up late so the |
|||
# name shows up properly here. Success! |
|||
if module_name == important_module: |
|||
return True |
|||
|
|||
# Some python verisons will will clean up modules so early that the |
|||
# module name at that point is no longer set. Try guessing from |
|||
# the filename then. |
|||
filename = os.path.abspath(tb.tb_frame.f_code.co_filename) |
|||
test_string = os.path.sep + important_module.replace('.', os.path.sep) |
|||
return test_string + '.py' in filename or \ |
|||
test_string + os.path.sep + '__init__.py' in filename |
@ -1,36 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
flask.globals |
|||
~~~~~~~~~~~~~ |
|||
|
|||
Defines all the global objects that are proxies to the current |
|||
active context. |
|||
|
|||
:copyright: (c) 2011 by Armin Ronacher. |
|||
:license: BSD, see LICENSE for more details. |
|||
""" |
|||
|
|||
from functools import partial |
|||
from werkzeug.local import LocalStack, LocalProxy |
|||
|
|||
def _lookup_object(name): |
|||
top = _request_ctx_stack.top |
|||
if top is None: |
|||
raise RuntimeError('working outside of request context') |
|||
return getattr(top, name) |
|||
|
|||
|
|||
def _find_app(): |
|||
top = _app_ctx_stack.top |
|||
if top is None: |
|||
raise RuntimeError('working outside of application context') |
|||
return top.app |
|||
|
|||
|
|||
# context locals |
|||
_request_ctx_stack = LocalStack() |
|||
_app_ctx_stack = LocalStack() |
|||
current_app = LocalProxy(_find_app) |
|||
request = LocalProxy(partial(_lookup_object, 'request')) |
|||
session = LocalProxy(partial(_lookup_object, 'session')) |
|||
g = LocalProxy(partial(_lookup_object, 'g')) |
@ -1,893 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
flask.helpers |
|||
~~~~~~~~~~~~~ |
|||
|
|||
Implements various helpers. |
|||
|
|||
:copyright: (c) 2011 by Armin Ronacher. |
|||
:license: BSD, see LICENSE for more details. |
|||
""" |
|||
|
|||
from __future__ import with_statement |
|||
|
|||
import os |
|||
import sys |
|||
import pkgutil |
|||
import posixpath |
|||
import mimetypes |
|||
from time import time |
|||
from zlib import adler32 |
|||
from threading import RLock |
|||
from werkzeug.routing import BuildError |
|||
from werkzeug.urls import url_quote |
|||
from functools import update_wrapper |
|||
|
|||
# try to load the best simplejson implementation available. If JSON |
|||
# is not installed, we add a failing class. |
|||
json_available = True |
|||
json = None |
|||
try: |
|||
import simplejson as json |
|||
except ImportError: |
|||
try: |
|||
import json |
|||
except ImportError: |
|||
try: |
|||
# Google Appengine offers simplejson via django |
|||
from django.utils import simplejson as json |
|||
except ImportError: |
|||
json_available = False |
|||
|
|||
|
|||
from werkzeug.datastructures import Headers |
|||
from werkzeug.exceptions import NotFound |
|||
|
|||
# this was moved in 0.7 |
|||
try: |
|||
from werkzeug.wsgi import wrap_file |
|||
except ImportError: |
|||
from werkzeug.utils import wrap_file |
|||
|
|||
from jinja2 import FileSystemLoader |
|||
|
|||
from .globals import session, _request_ctx_stack, _app_ctx_stack, \ |
|||
current_app, request |
|||
|
|||
|
|||
def _assert_have_json(): |
|||
"""Helper function that fails if JSON is unavailable.""" |
|||
if not json_available: |
|||
raise RuntimeError('simplejson not installed') |
|||
|
|||
|
|||
# figure out if simplejson escapes slashes. This behavior was changed |
|||
# from one version to another without reason. |
|||
if not json_available or '\\/' not in json.dumps('/'): |
|||
|
|||
def _tojson_filter(*args, **kwargs): |
|||
if __debug__: |
|||
_assert_have_json() |
|||
return json.dumps(*args, **kwargs).replace('/', '\\/') |
|||
else: |
|||
_tojson_filter = json.dumps |
|||
|
|||
|
|||
# sentinel |
|||
_missing = object() |
|||
|
|||
|
|||
# what separators does this operating system provide that are not a slash? |
|||
# this is used by the send_from_directory function to ensure that nobody is |
|||
# able to access files from outside the filesystem. |
|||
_os_alt_seps = list(sep for sep in [os.path.sep, os.path.altsep] |
|||
if sep not in (None, '/')) |
|||
|
|||
|
|||
def _endpoint_from_view_func(view_func): |
|||
"""Internal helper that returns the default endpoint for a given |
|||
function. This always is the function name. |
|||
""" |
|||
assert view_func is not None, 'expected view func if endpoint ' \ |
|||
'is not provided.' |
|||
return view_func.__name__ |
|||
|
|||
|
|||
def stream_with_context(generator_or_function): |
|||
"""Request contexts disappear when the response is started on the server. |
|||
This is done for efficiency reasons and to make it less likely to encounter |
|||
memory leaks with badly written WSGI middlewares. The downside is that if |
|||
you are using streamed responses, the generator cannot access request bound |
|||
information any more. |
|||
|
|||
This function however can help you keep the context around for longer:: |
|||
|
|||
from flask import stream_with_context, request, Response |
|||
|
|||
@app.route('/stream') |
|||
def streamed_response(): |
|||
@stream_with_context |
|||
def generate(): |
|||
yield 'Hello ' |
|||
yield request.args['name'] |
|||
yield '!' |
|||
return Response(generate()) |
|||
|
|||
Alternatively it can also be used around a specific generator: |
|||
|
|||
from flask import stream_with_context, request, Response |
|||
|
|||
@app.route('/stream') |
|||
def streamed_response(): |
|||
def generate(): |
|||
yield 'Hello ' |
|||
yield request.args['name'] |
|||
yield '!' |
|||
return Response(stream_with_context(generate())) |
|||
|
|||
.. versionadded:: 0.9 |
|||
""" |
|||
try: |
|||
gen = iter(generator_or_function) |
|||
except TypeError: |
|||
def decorator(*args, **kwargs): |
|||
gen = generator_or_function() |
|||
return stream_with_context(gen) |
|||
return update_wrapper(decorator, generator_or_function) |
|||
|
|||
def generator(): |
|||
ctx = _request_ctx_stack.top |
|||
if ctx is None: |
|||
raise RuntimeError('Attempted to stream with context but ' |
|||
'there was no context in the first place to keep around.') |
|||
with ctx: |
|||
# Dummy sentinel. Has to be inside the context block or we're |
|||
# not actually keeping the context around. |
|||
yield None |
|||
|
|||
# The try/finally is here so that if someone passes a WSGI level |
|||
# iterator in we're still running the cleanup logic. Generators |
|||
# don't need that because they are closed on their destruction |
|||
# automatically. |
|||
try: |
|||
for item in gen: |
|||
yield item |
|||
finally: |
|||
if hasattr(gen, 'close'): |
|||
gen.close() |
|||
|
|||
# The trick is to start the generator. Then the code execution runs until |
|||
# the first dummy None is yielded at which point the context was already |
|||
# pushed. This item is discarded. Then when the iteration continues the |
|||
# real generator is executed. |
|||
wrapped_g = generator() |
|||
wrapped_g.next() |
|||
return wrapped_g |
|||
|
|||
|
|||
def jsonify(*args, **kwargs): |
|||
"""Creates a :class:`~flask.Response` with the JSON representation of |
|||
the given arguments with an `application/json` mimetype. The arguments |
|||
to this function are the same as to the :class:`dict` constructor. |
|||
|
|||
Example usage:: |
|||
|
|||
@app.route('/_get_current_user') |
|||
def get_current_user(): |
|||
return jsonify(username=g.user.username, |
|||
email=g.user.email, |
|||
id=g.user.id) |
|||
|
|||
This will send a JSON response like this to the browser:: |
|||
|
|||
{ |
|||
"username": "admin", |
|||
"email": "admin@localhost", |
|||
"id": 42 |
|||
} |
|||
|
|||
This requires Python 2.6 or an installed version of simplejson. For |
|||
security reasons only objects are supported toplevel. For more |
|||
information about this, have a look at :ref:`json-security`. |
|||
|
|||
.. versionadded:: 0.2 |
|||
""" |
|||
if __debug__: |
|||
_assert_have_json() |
|||
return current_app.response_class(json.dumps(dict(*args, **kwargs), |
|||
indent=None if request.is_xhr else 2), mimetype='application/json') |
|||
|
|||
|
|||
def make_response(*args): |
|||
"""Sometimes it is necessary to set additional headers in a view. Because |
|||
views do not have to return response objects but can return a value that |
|||
is converted into a response object by Flask itself, it becomes tricky to |
|||
add headers to it. This function can be called instead of using a return |
|||
and you will get a response object which you can use to attach headers. |
|||
|
|||
If view looked like this and you want to add a new header:: |
|||
|
|||
def index(): |
|||
return render_template('index.html', foo=42) |
|||
|
|||
You can now do something like this:: |
|||
|
|||
def index(): |
|||
response = make_response(render_template('index.html', foo=42)) |
|||
response.headers['X-Parachutes'] = 'parachutes are cool' |
|||
return response |
|||
|
|||
This function accepts the very same arguments you can return from a |
|||
view function. This for example creates a response with a 404 error |
|||
code:: |
|||
|
|||
response = make_response(render_template('not_found.html'), 404) |
|||
|
|||
The other use case of this function is to force the return value of a |
|||
view function into a response which is helpful with view |
|||
decorators:: |
|||
|
|||
response = make_response(view_function()) |
|||
response.headers['X-Parachutes'] = 'parachutes are cool' |
|||
|
|||
Internally this function does the following things: |
|||
|
|||
- if no arguments are passed, it creates a new response argument |
|||
- if one argument is passed, :meth:`flask.Flask.make_response` |
|||
is invoked with it. |
|||
- if more than one argument is passed, the arguments are passed |
|||
to the :meth:`flask.Flask.make_response` function as tuple. |
|||
|
|||
.. versionadded:: 0.6 |
|||
""" |
|||
if not args: |
|||
return current_app.response_class() |
|||
if len(args) == 1: |
|||
args = args[0] |
|||
return current_app.make_response(args) |
|||
|
|||
|
|||
def url_for(endpoint, **values): |
|||
"""Generates a URL to the given endpoint with the method provided. |
|||
|
|||
Variable arguments that are unknown to the target endpoint are appended |
|||
to the generated URL as query arguments. If the value of a query argument |
|||
is `None`, the whole pair is skipped. In case blueprints are active |
|||
you can shortcut references to the same blueprint by prefixing the |
|||
local endpoint with a dot (``.``). |
|||
|
|||
This will reference the index function local to the current blueprint:: |
|||
|
|||
url_for('.index') |
|||
|
|||
For more information, head over to the :ref:`Quickstart <url-building>`. |
|||
|
|||
To integrate applications, :class:`Flask` has a hook to intercept URL build |
|||
errors through :attr:`Flask.build_error_handler`. The `url_for` function |
|||
results in a :exc:`~werkzeug.routing.BuildError` when the current app does |
|||
not have a URL for the given endpoint and values. When it does, the |
|||
:data:`~flask.current_app` calls its :attr:`~Flask.build_error_handler` if |
|||
it is not `None`, which can return a string to use as the result of |
|||
`url_for` (instead of `url_for`'s default to raise the |
|||
:exc:`~werkzeug.routing.BuildError` exception) or re-raise the exception. |
|||
An example:: |
|||
|
|||
def external_url_handler(error, endpoint, **values): |
|||
"Looks up an external URL when `url_for` cannot build a URL." |
|||
# This is an example of hooking the build_error_handler. |
|||
# Here, lookup_url is some utility function you've built |
|||
# which looks up the endpoint in some external URL registry. |
|||
url = lookup_url(endpoint, **values) |
|||
if url is None: |
|||
# External lookup did not have a URL. |
|||
# Re-raise the BuildError, in context of original traceback. |
|||
exc_type, exc_value, tb = sys.exc_info() |
|||
if exc_value is error: |
|||
raise exc_type, exc_value, tb |
|||
else: |
|||
raise error |
|||
# url_for will use this result, instead of raising BuildError. |
|||
return url |
|||
|
|||
app.build_error_handler = external_url_handler |
|||
|
|||
Here, `error` is the instance of :exc:`~werkzeug.routing.BuildError`, and |
|||
`endpoint` and `**values` are the arguments passed into `url_for`. Note |
|||
that this is for building URLs outside the current application, and not for |
|||
handling 404 NotFound errors. |
|||
|
|||
.. versionadded:: 0.9 |
|||
The `_anchor` and `_method` parameters were added. |
|||
|
|||
.. versionadded:: 0.9 |
|||
Calls :meth:`Flask.handle_build_error` on |
|||
:exc:`~werkzeug.routing.BuildError`. |
|||
|
|||
:param endpoint: the endpoint of the URL (name of the function) |
|||
:param values: the variable arguments of the URL rule |
|||
:param _external: if set to `True`, an absolute URL is generated. |
|||
:param _anchor: if provided this is added as anchor to the URL. |
|||
:param _method: if provided this explicitly specifies an HTTP method. |
|||
""" |
|||
appctx = _app_ctx_stack.top |
|||
reqctx = _request_ctx_stack.top |
|||
if appctx is None: |
|||
raise RuntimeError('Attempted to generate a URL with the application ' |
|||
'context being pushed. This has to be executed ') |
|||
|
|||
# If request specific information is available we have some extra |
|||
# features that support "relative" urls. |
|||
if reqctx is not None: |
|||
url_adapter = reqctx.url_adapter |
|||
blueprint_name = request.blueprint |
|||
if not reqctx.request._is_old_module: |
|||
if endpoint[:1] == '.': |
|||
if blueprint_name is not None: |
|||
endpoint = blueprint_name + endpoint |
|||
else: |
|||
endpoint = endpoint[1:] |
|||
else: |
|||
# TODO: get rid of this deprecated functionality in 1.0 |
|||
if '.' not in endpoint: |
|||
if blueprint_name is not None: |
|||
endpoint = blueprint_name + '.' + endpoint |
|||
elif endpoint.startswith('.'): |
|||
endpoint = endpoint[1:] |
|||
external = values.pop('_external', False) |
|||
|
|||
# Otherwise go with the url adapter from the appctx and make |
|||
# the urls external by default. |
|||
else: |
|||
url_adapter = appctx.url_adapter |
|||
if url_adapter is None: |
|||
raise RuntimeError('Application was not able to create a URL ' |
|||
'adapter for request independent URL generation. ' |
|||
'You might be able to fix this by setting ' |
|||
'the SERVER_NAME config variable.') |
|||
external = values.pop('_external', True) |
|||
|
|||
anchor = values.pop('_anchor', None) |
|||
method = values.pop('_method', None) |
|||
appctx.app.inject_url_defaults(endpoint, values) |
|||
try: |
|||
rv = url_adapter.build(endpoint, values, method=method, |
|||
force_external=external) |
|||
except BuildError, error: |
|||
# We need to inject the values again so that the app callback can |
|||
# deal with that sort of stuff. |
|||
values['_external'] = external |
|||
values['_anchor'] = anchor |
|||
values['_method'] = method |
|||
return appctx.app.handle_url_build_error(error, endpoint, values) |
|||
|
|||
rv = url_adapter.build(endpoint, values, method=method, |
|||
force_external=external) |
|||
if anchor is not None: |
|||
rv += '#' + url_quote(anchor) |
|||
return rv |
|||
|
|||
|
|||
def get_template_attribute(template_name, attribute): |
|||
"""Loads a macro (or variable) a template exports. This can be used to |
|||
invoke a macro from within Python code. If you for example have a |
|||
template named `_cider.html` with the following contents: |
|||
|
|||
.. sourcecode:: html+jinja |
|||
|
|||
{% macro hello(name) %}Hello {{ name }}!{% endmacro %} |
|||
|
|||
You can access this from Python code like this:: |
|||
|
|||
hello = get_template_attribute('_cider.html', 'hello') |
|||
return hello('World') |
|||
|
|||
.. versionadded:: 0.2 |
|||
|
|||
:param template_name: the name of the template |
|||
:param attribute: the name of the variable of macro to acccess |
|||
""" |
|||
return getattr(current_app.jinja_env.get_template(template_name).module, |
|||
attribute) |
|||
|
|||
|
|||
def flash(message, category='message'): |
|||
"""Flashes a message to the next request. In order to remove the |
|||
flashed message from the session and to display it to the user, |
|||
the template has to call :func:`get_flashed_messages`. |
|||
|
|||
.. versionchanged:: 0.3 |
|||
`category` parameter added. |
|||
|
|||
:param message: the message to be flashed. |
|||
:param category: the category for the message. The following values |
|||
are recommended: ``'message'`` for any kind of message, |
|||
``'error'`` for errors, ``'info'`` for information |
|||
messages and ``'warning'`` for warnings. However any |
|||
kind of string can be used as category. |
|||
""" |
|||
# Original implementation: |
|||
# |
|||
# session.setdefault('_flashes', []).append((category, message)) |
|||
# |
|||
# This assumed that changes made to mutable structures in the session are |
|||
# are always in sync with the sess on object, which is not true for session |
|||
# implementations that use external storage for keeping their keys/values. |
|||
flashes = session.get('_flashes', []) |
|||
flashes.append((category, message)) |
|||
session['_flashes'] = flashes |
|||
|
|||
|
|||
def get_flashed_messages(with_categories=False, category_filter=[]): |
|||
"""Pulls all flashed messages from the session and returns them. |
|||
Further calls in the same request to the function will return |
|||
the same messages. By default just the messages are returned, |
|||
but when `with_categories` is set to `True`, the return value will |
|||
be a list of tuples in the form ``(category, message)`` instead. |
|||
|
|||
Filter the flashed messages to one or more categories by providing those |
|||
categories in `category_filter`. This allows rendering categories in |
|||
separate html blocks. The `with_categories` and `category_filter` |
|||
arguments are distinct: |
|||
|
|||
* `with_categories` controls whether categories are returned with message |
|||
text (`True` gives a tuple, where `False` gives just the message text). |
|||
* `category_filter` filters the messages down to only those matching the |
|||
provided categories. |
|||
|
|||
See :ref:`message-flashing-pattern` for examples. |
|||
|
|||
.. versionchanged:: 0.3 |
|||
`with_categories` parameter added. |
|||
|
|||
.. versionchanged:: 0.9 |
|||
`category_filter` parameter added. |
|||
|
|||
:param with_categories: set to `True` to also receive categories. |
|||
:param category_filter: whitelist of categories to limit return values |
|||
""" |
|||
flashes = _request_ctx_stack.top.flashes |
|||
if flashes is None: |
|||
_request_ctx_stack.top.flashes = flashes = session.pop('_flashes') \ |
|||
if '_flashes' in session else [] |
|||
if category_filter: |
|||
flashes = filter(lambda f: f[0] in category_filter, flashes) |
|||
if not with_categories: |
|||
return [x[1] for x in flashes] |
|||
return flashes |
|||
|
|||
|
|||
def send_file(filename_or_fp, mimetype=None, as_attachment=False, |
|||
attachment_filename=None, add_etags=True, |
|||
cache_timeout=None, conditional=False): |
|||
"""Sends the contents of a file to the client. This will use the |
|||
most efficient method available and configured. By default it will |
|||
try to use the WSGI server's file_wrapper support. Alternatively |
|||
you can set the application's :attr:`~Flask.use_x_sendfile` attribute |
|||
to ``True`` to directly emit an `X-Sendfile` header. This however |
|||
requires support of the underlying webserver for `X-Sendfile`. |
|||
|
|||
By default it will try to guess the mimetype for you, but you can |
|||
also explicitly provide one. For extra security you probably want |
|||
to send certain files as attachment (HTML for instance). The mimetype |
|||
guessing requires a `filename` or an `attachment_filename` to be |
|||
provided. |
|||
|
|||
Please never pass filenames to this function from user sources without |
|||
checking them first. Something like this is usually sufficient to |
|||
avoid security problems:: |
|||
|
|||
if '..' in filename or filename.startswith('/'): |
|||
abort(404) |
|||
|
|||
.. versionadded:: 0.2 |
|||
|
|||
.. versionadded:: 0.5 |
|||
The `add_etags`, `cache_timeout` and `conditional` parameters were |
|||
added. The default behavior is now to attach etags. |
|||
|
|||
.. versionchanged:: 0.7 |
|||
mimetype guessing and etag support for file objects was |
|||
deprecated because it was unreliable. Pass a filename if you are |
|||
able to, otherwise attach an etag yourself. This functionality |
|||
will be removed in Flask 1.0 |
|||
|
|||
.. versionchanged:: 0.9 |
|||
cache_timeout pulls its default from application config, when None. |
|||
|
|||
:param filename_or_fp: the filename of the file to send. This is |
|||
relative to the :attr:`~Flask.root_path` if a |
|||
relative path is specified. |
|||
Alternatively a file object might be provided |
|||
in which case `X-Sendfile` might not work and |
|||
fall back to the traditional method. Make sure |
|||
that the file pointer is positioned at the start |
|||
of data to send before calling :func:`send_file`. |
|||
:param mimetype: the mimetype of the file if provided, otherwise |
|||
auto detection happens. |
|||
:param as_attachment: set to `True` if you want to send this file with |
|||
a ``Content-Disposition: attachment`` header. |
|||
:param attachment_filename: the filename for the attachment if it |
|||
differs from the file's filename. |
|||
:param add_etags: set to `False` to disable attaching of etags. |
|||
:param conditional: set to `True` to enable conditional responses. |
|||
|
|||
:param cache_timeout: the timeout in seconds for the headers. When `None` |
|||
(default), this value is set by |
|||
:meth:`~Flask.get_send_file_max_age` of |
|||
:data:`~flask.current_app`. |
|||
""" |
|||
mtime = None |
|||
if isinstance(filename_or_fp, basestring): |
|||
filename = filename_or_fp |
|||
file = None |
|||
else: |
|||
from warnings import warn |
|||
file = filename_or_fp |
|||
filename = getattr(file, 'name', None) |
|||
|
|||
# XXX: this behavior is now deprecated because it was unreliable. |
|||
# removed in Flask 1.0 |
|||
if not attachment_filename and not mimetype \ |
|||
and isinstance(filename, basestring): |
|||
warn(DeprecationWarning('The filename support for file objects ' |
|||
'passed to send_file is now deprecated. Pass an ' |
|||
'attach_filename if you want mimetypes to be guessed.'), |
|||
stacklevel=2) |
|||
if add_etags: |
|||
warn(DeprecationWarning('In future flask releases etags will no ' |
|||
'longer be generated for file objects passed to the send_file ' |
|||
'function because this behavior was unreliable. Pass ' |
|||
'filenames instead if possible, otherwise attach an etag ' |
|||
'yourself based on another value'), stacklevel=2) |
|||
|
|||
if filename is not None: |
|||
if not os.path.isabs(filename): |
|||
filename = os.path.join(current_app.root_path, filename) |
|||
if mimetype is None and (filename or attachment_filename): |
|||
mimetype = mimetypes.guess_type(filename or attachment_filename)[0] |
|||
if mimetype is None: |
|||
mimetype = 'application/octet-stream' |
|||
|
|||
headers = Headers() |
|||
if as_attachment: |
|||
if attachment_filename is None: |
|||
if filename is None: |
|||
raise TypeError('filename unavailable, required for ' |
|||
'sending as attachment') |
|||
attachment_filename = os.path.basename(filename) |
|||
headers.add('Content-Disposition', 'attachment', |
|||
filename=attachment_filename) |
|||
|
|||
if current_app.use_x_sendfile and filename: |
|||
if file is not None: |
|||
file.close() |
|||
headers['X-Sendfile'] = filename |
|||
data = None |
|||
else: |
|||
if file is None: |
|||
file = open(filename, 'rb') |
|||
mtime = os.path.getmtime(filename) |
|||
data = wrap_file(request.environ, file) |
|||
|
|||
rv = current_app.response_class(data, mimetype=mimetype, headers=headers, |
|||
direct_passthrough=True) |
|||
|
|||
# if we know the file modification date, we can store it as the |
|||
# the time of the last modification. |
|||
if mtime is not None: |
|||
rv.last_modified = int(mtime) |
|||
|
|||
rv.cache_control.public = True |
|||
if cache_timeout is None: |
|||
cache_timeout = current_app.get_send_file_max_age(filename) |
|||
if cache_timeout is not None: |
|||
rv.cache_control.max_age = cache_timeout |
|||
rv.expires = int(time() + cache_timeout) |
|||
|
|||
if add_etags and filename is not None: |
|||
rv.set_etag('flask-%s-%s-%s' % ( |
|||
os.path.getmtime(filename), |
|||
os.path.getsize(filename), |
|||
adler32( |
|||
filename.encode('utf8') if isinstance(filename, unicode) |
|||
else filename |
|||
) & 0xffffffff |
|||
)) |
|||
if conditional: |
|||
rv = rv.make_conditional(request) |
|||
# make sure we don't send x-sendfile for servers that |
|||
# ignore the 304 status code for x-sendfile. |
|||
if rv.status_code == 304: |
|||
rv.headers.pop('x-sendfile', None) |
|||
return rv |
|||
|
|||
|
|||
def safe_join(directory, filename): |
|||
"""Safely join `directory` and `filename`. |
|||
|
|||
Example usage:: |
|||
|
|||
@app.route('/wiki/<path:filename>') |
|||
def wiki_page(filename): |
|||
filename = safe_join(app.config['WIKI_FOLDER'], filename) |
|||
with open(filename, 'rb') as fd: |
|||
content = fd.read() # Read and process the file content... |
|||
|
|||
:param directory: the base directory. |
|||
:param filename: the untrusted filename relative to that directory. |
|||
:raises: :class:`~werkzeug.exceptions.NotFound` if the resulting path |
|||
would fall out of `directory`. |
|||
""" |
|||
filename = posixpath.normpath(filename) |
|||
for sep in _os_alt_seps: |
|||
if sep in filename: |
|||
raise NotFound() |
|||
if os.path.isabs(filename) or filename.startswith('../'): |
|||
raise NotFound() |
|||
return os.path.join(directory, filename) |
|||
|
|||
|
|||
def send_from_directory(directory, filename, **options): |
|||
"""Send a file from a given directory with :func:`send_file`. This |
|||
is a secure way to quickly expose static files from an upload folder |
|||
or something similar. |
|||
|
|||
Example usage:: |
|||
|
|||
@app.route('/uploads/<path:filename>') |
|||
def download_file(filename): |
|||
return send_from_directory(app.config['UPLOAD_FOLDER'], |
|||
filename, as_attachment=True) |
|||
|
|||
.. admonition:: Sending files and Performance |
|||
|
|||
It is strongly recommended to activate either `X-Sendfile` support in |
|||
your webserver or (if no authentication happens) to tell the webserver |
|||
to serve files for the given path on its own without calling into the |
|||
web application for improved performance. |
|||
|
|||
.. versionadded:: 0.5 |
|||
|
|||
:param directory: the directory where all the files are stored. |
|||
:param filename: the filename relative to that directory to |
|||
download. |
|||
:param options: optional keyword arguments that are directly |
|||
forwarded to :func:`send_file`. |
|||
""" |
|||
filename = safe_join(directory, filename) |
|||
if not os.path.isfile(filename): |
|||
raise NotFound() |
|||
options.setdefault('conditional', True) |
|||
return send_file(filename, **options) |
|||
|
|||
|
|||
def get_root_path(import_name): |
|||
"""Returns the path to a package or cwd if that cannot be found. This |
|||
returns the path of a package or the folder that contains a module. |
|||
|
|||
Not to be confused with the package path returned by :func:`find_package`. |
|||
""" |
|||
# Module already imported and has a file attribute. Use that first. |
|||
mod = sys.modules.get(import_name) |
|||
if mod is not None and hasattr(mod, '__file__'): |
|||
return os.path.dirname(os.path.abspath(mod.__file__)) |
|||
|
|||
# Next attempt: check the loader. |
|||
loader = pkgutil.get_loader(import_name) |
|||
|
|||
# Loader does not exist or we're referring to an unloaded main module |
|||
# or a main module without path (interactive sessions), go with the |
|||
# current working directory. |
|||
if loader is None or import_name == '__main__': |
|||
return os.getcwd() |
|||
|
|||
# For .egg, zipimporter does not have get_filename until Python 2.7. |
|||
# Some other loaders might exhibit the same behavior. |
|||
if hasattr(loader, 'get_filename'): |
|||
filepath = loader.get_filename(import_name) |
|||
else: |
|||
# Fall back to imports. |
|||
__import__(import_name) |
|||
filepath = sys.modules[import_name].__file__ |
|||
|
|||
# filepath is import_name.py for a module, or __init__.py for a package. |
|||
return os.path.dirname(os.path.abspath(filepath)) |
|||
|
|||
|
|||
def find_package(import_name): |
|||
"""Finds a package and returns the prefix (or None if the package is |
|||
not installed) as well as the folder that contains the package or |
|||
module as a tuple. The package path returned is the module that would |
|||
have to be added to the pythonpath in order to make it possible to |
|||
import the module. The prefix is the path below which a UNIX like |
|||
folder structure exists (lib, share etc.). |
|||
""" |
|||
root_mod_name = import_name.split('.')[0] |
|||
loader = pkgutil.get_loader(root_mod_name) |
|||
if loader is None or import_name == '__main__': |
|||
# import name is not found, or interactive/main module |
|||
package_path = os.getcwd() |
|||
else: |
|||
# For .egg, zipimporter does not have get_filename until Python 2.7. |
|||
if hasattr(loader, 'get_filename'): |
|||
filename = loader.get_filename(root_mod_name) |
|||
elif hasattr(loader, 'archive'): |
|||
# zipimporter's loader.archive points to the .egg or .zip |
|||
# archive filename is dropped in call to dirname below. |
|||
filename = loader.archive |
|||
else: |
|||
# At least one loader is missing both get_filename and archive: |
|||
# Google App Engine's HardenedModulesHook |
|||
# |
|||
# Fall back to imports. |
|||
__import__(import_name) |
|||
filename = sys.modules[import_name].__file__ |
|||
package_path = os.path.abspath(os.path.dirname(filename)) |
|||
# package_path ends with __init__.py for a package |
|||
if loader.is_package(root_mod_name): |
|||
package_path = os.path.dirname(package_path) |
|||
|
|||
site_parent, site_folder = os.path.split(package_path) |
|||
py_prefix = os.path.abspath(sys.prefix) |
|||
if package_path.startswith(py_prefix): |
|||
return py_prefix, package_path |
|||
elif site_folder.lower() == 'site-packages': |
|||
parent, folder = os.path.split(site_parent) |
|||
# Windows like installations |
|||
if folder.lower() == 'lib': |
|||
base_dir = parent |
|||
# UNIX like installations |
|||
elif os.path.basename(parent).lower() == 'lib': |
|||
base_dir = os.path.dirname(parent) |
|||
else: |
|||
base_dir = site_parent |
|||
return base_dir, package_path |
|||
return None, package_path |
|||
|
|||
|
|||
class locked_cached_property(object): |
|||
"""A decorator that converts a function into a lazy property. The |
|||
function wrapped is called the first time to retrieve the result |
|||
and then that calculated result is used the next time you access |
|||
the value. Works like the one in Werkzeug but has a lock for |
|||
thread safety. |
|||
""" |
|||
|
|||
def __init__(self, func, name=None, doc=None): |
|||
self.__name__ = name or func.__name__ |
|||
self.__module__ = func.__module__ |
|||
self.__doc__ = doc or func.__doc__ |
|||
self.func = func |
|||
self.lock = RLock() |
|||
|
|||
def __get__(self, obj, type=None): |
|||
if obj is None: |
|||
return self |
|||
with self.lock: |
|||
value = obj.__dict__.get(self.__name__, _missing) |
|||
if value is _missing: |
|||
value = self.func(obj) |
|||
obj.__dict__[self.__name__] = value |
|||
return value |
|||
|
|||
|
|||
class _PackageBoundObject(object): |
|||
|
|||
def __init__(self, import_name, template_folder=None): |
|||
#: The name of the package or module. Do not change this once |
|||
#: it was set by the constructor. |
|||
self.import_name = import_name |
|||
|
|||
#: location of the templates. `None` if templates should not be |
|||
#: exposed. |
|||
self.template_folder = template_folder |
|||
|
|||
#: Where is the app root located? |
|||
self.root_path = get_root_path(self.import_name) |
|||
|
|||
self._static_folder = None |
|||
self._static_url_path = None |
|||
|
|||
def _get_static_folder(self): |
|||
if self._static_folder is not None: |
|||
return os.path.join(self.root_path, self._static_folder) |
|||
def _set_static_folder(self, value): |
|||
self._static_folder = value |
|||
static_folder = property(_get_static_folder, _set_static_folder) |
|||
del _get_static_folder, _set_static_folder |
|||
|
|||
def _get_static_url_path(self): |
|||
if self._static_url_path is None: |
|||
if self.static_folder is None: |
|||
return None |
|||
return '/' + os.path.basename(self.static_folder) |
|||
return self._static_url_path |
|||
def _set_static_url_path(self, value): |
|||
self._static_url_path = value |
|||
static_url_path = property(_get_static_url_path, _set_static_url_path) |
|||
del _get_static_url_path, _set_static_url_path |
|||
|
|||
@property |
|||
def has_static_folder(self): |
|||
"""This is `True` if the package bound object's container has a |
|||
folder named ``'static'``. |
|||
|
|||
.. versionadded:: 0.5 |
|||
""" |
|||
return self.static_folder is not None |
|||
|
|||
@locked_cached_property |
|||
def jinja_loader(self): |
|||
"""The Jinja loader for this package bound object. |
|||
|
|||
.. versionadded:: 0.5 |
|||
""" |
|||
if self.template_folder is not None: |
|||
return FileSystemLoader(os.path.join(self.root_path, |
|||
self.template_folder)) |
|||
|
|||
def get_send_file_max_age(self, filename): |
|||
"""Provides default cache_timeout for the :func:`send_file` functions. |
|||
|
|||
By default, this function returns ``SEND_FILE_MAX_AGE_DEFAULT`` from |
|||
the configuration of :data:`~flask.current_app`. |
|||
|
|||
Static file functions such as :func:`send_from_directory` use this |
|||
function, and :func:`send_file` calls this function on |
|||
:data:`~flask.current_app` when the given cache_timeout is `None`. If a |
|||
cache_timeout is given in :func:`send_file`, that timeout is used; |
|||
otherwise, this method is called. |
|||
|
|||
This allows subclasses to change the behavior when sending files based |
|||
on the filename. For example, to set the cache timeout for .js files |
|||
to 60 seconds:: |
|||
|
|||
class MyFlask(flask.Flask): |
|||
def get_send_file_max_age(self, name): |
|||
if name.lower().endswith('.js'): |
|||
return 60 |
|||
return flask.Flask.get_send_file_max_age(self, name) |
|||
|
|||
.. versionadded:: 0.9 |
|||
""" |
|||
return current_app.config['SEND_FILE_MAX_AGE_DEFAULT'] |
|||
|
|||
def send_static_file(self, filename): |
|||
"""Function used internally to send static files from the static |
|||
folder to the browser. |
|||
|
|||
.. versionadded:: 0.5 |
|||
""" |
|||
if not self.has_static_folder: |
|||
raise RuntimeError('No static folder for this object') |
|||
# Ensure get_send_file_max_age is called in all cases. |
|||
# Here, we ensure get_send_file_max_age is called for Blueprints. |
|||
cache_timeout = self.get_send_file_max_age(filename) |
|||
return send_from_directory(self.static_folder, filename, |
|||
cache_timeout=cache_timeout) |
|||
|
|||
def open_resource(self, resource, mode='rb'): |
|||
"""Opens a resource from the application's resource folder. To see |
|||
how this works, consider the following folder structure:: |
|||
|
|||
/myapplication.py |
|||
/schema.sql |
|||
/static |
|||
/style.css |
|||
/templates |
|||
/layout.html |
|||
/index.html |
|||
|
|||
If you want to open the `schema.sql` file you would do the |
|||
following:: |
|||
|
|||
with app.open_resource('schema.sql') as f: |
|||
contents = f.read() |
|||
do_something_with(contents) |
|||
|
|||
:param resource: the name of the resource. To access resources within |
|||
subfolders use forward slashes as separator. |
|||
""" |
|||
if mode not in ('r', 'rb'): |
|||
raise ValueError('Resources can only be opened for reading') |
|||
return open(os.path.join(self.root_path, resource), mode) |
@ -1,45 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
flask.logging |
|||
~~~~~~~~~~~~~ |
|||
|
|||
Implements the logging support for Flask. |
|||
|
|||
:copyright: (c) 2011 by Armin Ronacher. |
|||
:license: BSD, see LICENSE for more details. |
|||
""" |
|||
|
|||
from __future__ import absolute_import |
|||
|
|||
from logging import getLogger, StreamHandler, Formatter, getLoggerClass, DEBUG |
|||
|
|||
|
|||
def create_logger(app): |
|||
"""Creates a logger for the given application. This logger works |
|||
similar to a regular Python logger but changes the effective logging |
|||
level based on the application's debug flag. Furthermore this |
|||
function also removes all attached handlers in case there was a |
|||
logger with the log name before. |
|||
""" |
|||
Logger = getLoggerClass() |
|||
|
|||
class DebugLogger(Logger): |
|||
def getEffectiveLevel(x): |
|||
if x.level == 0 and app.debug: |
|||
return DEBUG |
|||
return Logger.getEffectiveLevel(x) |
|||
|
|||
class DebugHandler(StreamHandler): |
|||
def emit(x, record): |
|||
StreamHandler.emit(x, record) if app.debug else None |
|||
|
|||
handler = DebugHandler() |
|||
handler.setLevel(DEBUG) |
|||
handler.setFormatter(Formatter(app.debug_log_format)) |
|||
logger = getLogger(app.logger_name) |
|||
# just in case that was not a new logger, get rid of all the handlers |
|||
# already attached to it. |
|||
del logger.handlers[:] |
|||
logger.__class__ = DebugLogger |
|||
logger.addHandler(handler) |
|||
return logger |
@ -1,42 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
flask.module |
|||
~~~~~~~~~~~~ |
|||
|
|||
Implements a class that represents module blueprints. |
|||
|
|||
:copyright: (c) 2011 by Armin Ronacher. |
|||
:license: BSD, see LICENSE for more details. |
|||
""" |
|||
|
|||
import os |
|||
|
|||
from .blueprints import Blueprint |
|||
|
|||
|
|||
def blueprint_is_module(bp): |
|||
"""Used to figure out if something is actually a module""" |
|||
return isinstance(bp, Module) |
|||
|
|||
|
|||
class Module(Blueprint): |
|||
"""Deprecated module support. Until Flask 0.6 modules were a different |
|||
name of the concept now available as blueprints in Flask. They are |
|||
essentially doing the same but have some bad semantics for templates and |
|||
static files that were fixed with blueprints. |
|||
|
|||
.. versionchanged:: 0.7 |
|||
Modules were deprecated in favor for blueprints. |
|||
""" |
|||
|
|||
def __init__(self, import_name, name=None, url_prefix=None, |
|||
static_path=None, subdomain=None): |
|||
if name is None: |
|||
assert '.' in import_name, 'name required if package name ' \ |
|||
'does not point to a submodule' |
|||
name = import_name.rsplit('.', 1)[1] |
|||
Blueprint.__init__(self, name, import_name, url_prefix=url_prefix, |
|||
subdomain=subdomain, template_folder='templates') |
|||
|
|||
if os.path.isdir(os.path.join(self.root_path, 'static')): |
|||
self._static_folder = 'static' |
@ -1,19 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
flask.session |
|||
~~~~~~~~~~~~~ |
|||
|
|||
This module used to flask with the session global so we moved it |
|||
over to flask.sessions |
|||
|
|||
:copyright: (c) 2011 by Armin Ronacher. |
|||
:license: BSD, see LICENSE for more details. |
|||
""" |
|||
|
|||
from warnings import warn |
|||
warn(DeprecationWarning('please use flask.sessions instead')) |
|||
|
|||
from .sessions import SecureCookieSession, NullSession |
|||
|
|||
Session = SecureCookieSession |
|||
_NullSession = NullSession |
@ -1,205 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
flask.sessions |
|||
~~~~~~~~~~~~~~ |
|||
|
|||
Implements cookie based sessions based on Werkzeug's secure cookie |
|||
system. |
|||
|
|||
:copyright: (c) 2011 by Armin Ronacher. |
|||
:license: BSD, see LICENSE for more details. |
|||
""" |
|||
|
|||
from datetime import datetime |
|||
from werkzeug.contrib.securecookie import SecureCookie |
|||
|
|||
|
|||
class SessionMixin(object): |
|||
"""Expands a basic dictionary with an accessors that are expected |
|||
by Flask extensions and users for the session. |
|||
""" |
|||
|
|||
def _get_permanent(self): |
|||
return self.get('_permanent', False) |
|||
|
|||
def _set_permanent(self, value): |
|||
self['_permanent'] = bool(value) |
|||
|
|||
#: this reflects the ``'_permanent'`` key in the dict. |
|||
permanent = property(_get_permanent, _set_permanent) |
|||
del _get_permanent, _set_permanent |
|||
|
|||
#: some session backends can tell you if a session is new, but that is |
|||
#: not necessarily guaranteed. Use with caution. The default mixin |
|||
#: implementation just hardcodes `False` in. |
|||
new = False |
|||
|
|||
#: for some backends this will always be `True`, but some backends will |
|||
#: default this to false and detect changes in the dictionary for as |
|||
#: long as changes do not happen on mutable structures in the session. |
|||
#: The default mixin implementation just hardcodes `True` in. |
|||
modified = True |
|||
|
|||
|
|||
class SecureCookieSession(SecureCookie, SessionMixin): |
|||
"""Expands the session with support for switching between permanent |
|||
and non-permanent sessions. |
|||
""" |
|||
|
|||
|
|||
class NullSession(SecureCookieSession): |
|||
"""Class used to generate nicer error messages if sessions are not |
|||
available. Will still allow read-only access to the empty session |
|||
but fail on setting. |
|||
""" |
|||
|
|||
def _fail(self, *args, **kwargs): |
|||
raise RuntimeError('the session is unavailable because no secret ' |
|||
'key was set. Set the secret_key on the ' |
|||
'application to something unique and secret.') |
|||
__setitem__ = __delitem__ = clear = pop = popitem = \ |
|||
update = setdefault = _fail |
|||
del _fail |
|||
|
|||
|
|||
class SessionInterface(object): |
|||
"""The basic interface you have to implement in order to replace the |
|||
default session interface which uses werkzeug's securecookie |
|||
implementation. The only methods you have to implement are |
|||
:meth:`open_session` and :meth:`save_session`, the others have |
|||
useful defaults which you don't need to change. |
|||
|
|||
The session object returned by the :meth:`open_session` method has to |
|||
provide a dictionary like interface plus the properties and methods |
|||
from the :class:`SessionMixin`. We recommend just subclassing a dict |
|||
and adding that mixin:: |
|||
|
|||
class Session(dict, SessionMixin): |
|||
pass |
|||
|
|||
If :meth:`open_session` returns `None` Flask will call into |
|||
:meth:`make_null_session` to create a session that acts as replacement |
|||
if the session support cannot work because some requirement is not |
|||
fulfilled. The default :class:`NullSession` class that is created |
|||
will complain that the secret key was not set. |
|||
|
|||
To replace the session interface on an application all you have to do |
|||
is to assign :attr:`flask.Flask.session_interface`:: |
|||
|
|||
app = Flask(__name__) |
|||
app.session_interface = MySessionInterface() |
|||
|
|||
.. versionadded:: 0.8 |
|||
""" |
|||
|
|||
#: :meth:`make_null_session` will look here for the class that should |
|||
#: be created when a null session is requested. Likewise the |
|||
#: :meth:`is_null_session` method will perform a typecheck against |
|||
#: this type. |
|||
null_session_class = NullSession |
|||
|
|||
def make_null_session(self, app): |
|||
"""Creates a null session which acts as a replacement object if the |
|||
real session support could not be loaded due to a configuration |
|||
error. This mainly aids the user experience because the job of the |
|||
null session is to still support lookup without complaining but |
|||
modifications are answered with a helpful error message of what |
|||
failed. |
|||
|
|||
This creates an instance of :attr:`null_session_class` by default. |
|||
""" |
|||
return self.null_session_class() |
|||
|
|||
def is_null_session(self, obj): |
|||
"""Checks if a given object is a null session. Null sessions are |
|||
not asked to be saved. |
|||
|
|||
This checks if the object is an instance of :attr:`null_session_class` |
|||
by default. |
|||
""" |
|||
return isinstance(obj, self.null_session_class) |
|||
|
|||
def get_cookie_domain(self, app): |
|||
"""Helpful helper method that returns the cookie domain that should |
|||
be used for the session cookie if session cookies are used. |
|||
""" |
|||
if app.config['SESSION_COOKIE_DOMAIN'] is not None: |
|||
return app.config['SESSION_COOKIE_DOMAIN'] |
|||
if app.config['SERVER_NAME'] is not None: |
|||
# chop of the port which is usually not supported by browsers |
|||
return '.' + app.config['SERVER_NAME'].rsplit(':', 1)[0] |
|||
|
|||
def get_cookie_path(self, app): |
|||
"""Returns the path for which the cookie should be valid. The |
|||
default implementation uses the value from the SESSION_COOKIE_PATH`` |
|||
config var if it's set, and falls back to ``APPLICATION_ROOT`` or |
|||
uses ``/`` if it's `None`. |
|||
""" |
|||
return app.config['SESSION_COOKIE_PATH'] or \ |
|||
app.config['APPLICATION_ROOT'] or '/' |
|||
|
|||
def get_cookie_httponly(self, app): |
|||
"""Returns True if the session cookie should be httponly. This |
|||
currently just returns the value of the ``SESSION_COOKIE_HTTPONLY`` |
|||
config var. |
|||
""" |
|||
return app.config['SESSION_COOKIE_HTTPONLY'] |
|||
|
|||
def get_cookie_secure(self, app): |
|||
"""Returns True if the cookie should be secure. This currently |
|||
just returns the value of the ``SESSION_COOKIE_SECURE`` setting. |
|||
""" |
|||
return app.config['SESSION_COOKIE_SECURE'] |
|||
|
|||
def get_expiration_time(self, app, session): |
|||
"""A helper method that returns an expiration date for the session |
|||
or `None` if the session is linked to the browser session. The |
|||
default implementation returns now + the permanent session |
|||
lifetime configured on the application. |
|||
""" |
|||
if session.permanent: |
|||
return datetime.utcnow() + app.permanent_session_lifetime |
|||
|
|||
def open_session(self, app, request): |
|||
"""This method has to be implemented and must either return `None` |
|||
in case the loading failed because of a configuration error or an |
|||
instance of a session object which implements a dictionary like |
|||
interface + the methods and attributes on :class:`SessionMixin`. |
|||
""" |
|||
raise NotImplementedError() |
|||
|
|||
def save_session(self, app, session, response): |
|||
"""This is called for actual sessions returned by :meth:`open_session` |
|||
at the end of the request. This is still called during a request |
|||
context so if you absolutely need access to the request you can do |
|||
that. |
|||
""" |
|||
raise NotImplementedError() |
|||
|
|||
|
|||
class SecureCookieSessionInterface(SessionInterface): |
|||
"""The cookie session interface that uses the Werkzeug securecookie |
|||
as client side session backend. |
|||
""" |
|||
session_class = SecureCookieSession |
|||
|
|||
def open_session(self, app, request): |
|||
key = app.secret_key |
|||
if key is not None: |
|||
return self.session_class.load_cookie(request, |
|||
app.session_cookie_name, |
|||
secret_key=key) |
|||
|
|||
def save_session(self, app, session, response): |
|||
expires = self.get_expiration_time(app, session) |
|||
domain = self.get_cookie_domain(app) |
|||
path = self.get_cookie_path(app) |
|||
httponly = self.get_cookie_httponly(app) |
|||
secure = self.get_cookie_secure(app) |
|||
if session.modified and not session: |
|||
response.delete_cookie(app.session_cookie_name, path=path, |
|||
domain=domain) |
|||
else: |
|||
session.save_cookie(response, app.session_cookie_name, path=path, |
|||
expires=expires, httponly=httponly, |
|||
secure=secure, domain=domain) |
@ -1,52 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
flask.signals |
|||
~~~~~~~~~~~~~ |
|||
|
|||
Implements signals based on blinker if available, otherwise |
|||
falls silently back to a noop |
|||
|
|||
:copyright: (c) 2011 by Armin Ronacher. |
|||
:license: BSD, see LICENSE for more details. |
|||
""" |
|||
signals_available = False |
|||
try: |
|||
from blinker import Namespace |
|||
signals_available = True |
|||
except ImportError: |
|||
class Namespace(object): |
|||
def signal(self, name, doc=None): |
|||
return _FakeSignal(name, doc) |
|||
|
|||
class _FakeSignal(object): |
|||
"""If blinker is unavailable, create a fake class with the same |
|||
interface that allows sending of signals but will fail with an |
|||
error on anything else. Instead of doing anything on send, it |
|||
will just ignore the arguments and do nothing instead. |
|||
""" |
|||
|
|||
def __init__(self, name, doc=None): |
|||
self.name = name |
|||
self.__doc__ = doc |
|||
def _fail(self, *args, **kwargs): |
|||
raise RuntimeError('signalling support is unavailable ' |
|||
'because the blinker library is ' |
|||
'not installed.') |
|||
send = lambda *a, **kw: None |
|||
connect = disconnect = has_receivers_for = receivers_for = \ |
|||
temporarily_connected_to = connected_to = _fail |
|||
del _fail |
|||
|
|||
# the namespace for code signals. If you are not flask code, do |
|||
# not put signals in here. Create your own namespace instead. |
|||
_signals = Namespace() |
|||
|
|||
|
|||
# core signals. For usage examples grep the sourcecode or consult |
|||
# the API documentation in docs/api.rst as well as docs/signals.rst |
|||
template_rendered = _signals.signal('template-rendered') |
|||
request_started = _signals.signal('request-started') |
|||
request_finished = _signals.signal('request-finished') |
|||
request_tearing_down = _signals.signal('request-tearing-down') |
|||
got_request_exception = _signals.signal('got-request-exception') |
|||
appcontext_tearing_down = _signals.signal('appcontext-tearing-down') |
@ -1,140 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
flask.templating |
|||
~~~~~~~~~~~~~~~~ |
|||
|
|||
Implements the bridge to Jinja2. |
|||
|
|||
:copyright: (c) 2011 by Armin Ronacher. |
|||
:license: BSD, see LICENSE for more details. |
|||
""" |
|||
import posixpath |
|||
from jinja2 import BaseLoader, Environment as BaseEnvironment, \ |
|||
TemplateNotFound |
|||
|
|||
from .globals import _request_ctx_stack |
|||
from .signals import template_rendered |
|||
from .module import blueprint_is_module |
|||
|
|||
|
|||
def _default_template_ctx_processor(): |
|||
"""Default template context processor. Injects `request`, |
|||
`session` and `g`. |
|||
""" |
|||
reqctx = _request_ctx_stack.top |
|||
return dict( |
|||
config=reqctx.app.config, |
|||
request=reqctx.request, |
|||
session=reqctx.session, |
|||
g=reqctx.g |
|||
) |
|||
|
|||
|
|||
class Environment(BaseEnvironment): |
|||
"""Works like a regular Jinja2 environment but has some additional |
|||
knowledge of how Flask's blueprint works so that it can prepend the |
|||
name of the blueprint to referenced templates if necessary. |
|||
""" |
|||
|
|||
def __init__(self, app, **options): |
|||
if 'loader' not in options: |
|||
options['loader'] = app.create_global_jinja_loader() |
|||
BaseEnvironment.__init__(self, **options) |
|||
self.app = app |
|||
|
|||
|
|||
class DispatchingJinjaLoader(BaseLoader): |
|||
"""A loader that looks for templates in the application and all |
|||
the blueprint folders. |
|||
""" |
|||
|
|||
def __init__(self, app): |
|||
self.app = app |
|||
|
|||
def get_source(self, environment, template): |
|||
for loader, local_name in self._iter_loaders(template): |
|||
try: |
|||
return loader.get_source(environment, local_name) |
|||
except TemplateNotFound: |
|||
pass |
|||
|
|||
raise TemplateNotFound(template) |
|||
|
|||
def _iter_loaders(self, template): |
|||
loader = self.app.jinja_loader |
|||
if loader is not None: |
|||
yield loader, template |
|||
|
|||
# old style module based loaders in case we are dealing with a |
|||
# blueprint that is an old style module |
|||
try: |
|||
module, local_name = posixpath.normpath(template).split('/', 1) |
|||
blueprint = self.app.blueprints[module] |
|||
if blueprint_is_module(blueprint): |
|||
loader = blueprint.jinja_loader |
|||
if loader is not None: |
|||
yield loader, local_name |
|||
except (ValueError, KeyError): |
|||
pass |
|||
|
|||
for blueprint in self.app.blueprints.itervalues(): |
|||
if blueprint_is_module(blueprint): |
|||
continue |
|||
loader = blueprint.jinja_loader |
|||
if loader is not None: |
|||
yield loader, template |
|||
|
|||
def list_templates(self): |
|||
result = set() |
|||
loader = self.app.jinja_loader |
|||
if loader is not None: |
|||
result.update(loader.list_templates()) |
|||
|
|||
for name, blueprint in self.app.blueprints.iteritems(): |
|||
loader = blueprint.jinja_loader |
|||
if loader is not None: |
|||
for template in loader.list_templates(): |
|||
prefix = '' |
|||
if blueprint_is_module(blueprint): |
|||
prefix = name + '/' |
|||
result.add(prefix + template) |
|||
|
|||
return list(result) |
|||
|
|||
|
|||
def _render(template, context, app): |
|||
"""Renders the template and fires the signal""" |
|||
rv = template.render(context) |
|||
template_rendered.send(app, template=template, context=context) |
|||
return rv |
|||
|
|||
|
|||
def render_template(template_name_or_list, **context): |
|||
"""Renders a template from the template folder with the given |
|||
context. |
|||
|
|||
:param template_name_or_list: the name of the template to be |
|||
rendered, or an iterable with template names |
|||
the first one existing will be rendered |
|||
:param context: the variables that should be available in the |
|||
context of the template. |
|||
""" |
|||
ctx = _request_ctx_stack.top |
|||
ctx.app.update_template_context(context) |
|||
return _render(ctx.app.jinja_env.get_or_select_template(template_name_or_list), |
|||
context, ctx.app) |
|||
|
|||
|
|||
def render_template_string(source, **context): |
|||
"""Renders a template from the given template source string |
|||
with the given context. |
|||
|
|||
:param template_name: the sourcecode of the template to be |
|||
rendered |
|||
:param context: the variables that should be available in the |
|||
context of the template. |
|||
""" |
|||
ctx = _request_ctx_stack.top |
|||
ctx.app.update_template_context(context) |
|||
return _render(ctx.app.jinja_env.from_string(source), |
|||
context, ctx.app) |
@ -1,118 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
flask.testing |
|||
~~~~~~~~~~~~~ |
|||
|
|||
Implements test support helpers. This module is lazily imported |
|||
and usually not used in production environments. |
|||
|
|||
:copyright: (c) 2011 by Armin Ronacher. |
|||
:license: BSD, see LICENSE for more details. |
|||
""" |
|||
|
|||
from __future__ import with_statement |
|||
|
|||
from contextlib import contextmanager |
|||
from werkzeug.test import Client, EnvironBuilder |
|||
from flask import _request_ctx_stack |
|||
|
|||
|
|||
def make_test_environ_builder(app, path='/', base_url=None, *args, **kwargs): |
|||
"""Creates a new test builder with some application defaults thrown in.""" |
|||
http_host = app.config.get('SERVER_NAME') |
|||
app_root = app.config.get('APPLICATION_ROOT') |
|||
if base_url is None: |
|||
base_url = 'http://%s/' % (http_host or 'localhost') |
|||
if app_root: |
|||
base_url += app_root.lstrip('/') |
|||
return EnvironBuilder(path, base_url, *args, **kwargs) |
|||
|
|||
|
|||
class FlaskClient(Client): |
|||
"""Works like a regular Werkzeug test client but has some knowledge about |
|||
how Flask works to defer the cleanup of the request context stack to the |
|||
end of a with body when used in a with statement. For general information |
|||
about how to use this class refer to :class:`werkzeug.test.Client`. |
|||
|
|||
Basic usage is outlined in the :ref:`testing` chapter. |
|||
""" |
|||
|
|||
preserve_context = False |
|||
|
|||
@contextmanager |
|||
def session_transaction(self, *args, **kwargs): |
|||
"""When used in combination with a with statement this opens a |
|||
session transaction. This can be used to modify the session that |
|||
the test client uses. Once the with block is left the session is |
|||
stored back. |
|||
|
|||
with client.session_transaction() as session: |
|||
session['value'] = 42 |
|||
|
|||
Internally this is implemented by going through a temporary test |
|||
request context and since session handling could depend on |
|||
request variables this function accepts the same arguments as |
|||
:meth:`~flask.Flask.test_request_context` which are directly |
|||
passed through. |
|||
""" |
|||
if self.cookie_jar is None: |
|||
raise RuntimeError('Session transactions only make sense ' |
|||
'with cookies enabled.') |
|||
app = self.application |
|||
environ_overrides = kwargs.setdefault('environ_overrides', {}) |
|||
self.cookie_jar.inject_wsgi(environ_overrides) |
|||
outer_reqctx = _request_ctx_stack.top |
|||
with app.test_request_context(*args, **kwargs) as c: |
|||
sess = app.open_session(c.request) |
|||
if sess is None: |
|||
raise RuntimeError('Session backend did not open a session. ' |
|||
'Check the configuration') |
|||
|
|||
# Since we have to open a new request context for the session |
|||
# handling we want to make sure that we hide out own context |
|||
# from the caller. By pushing the original request context |
|||
# (or None) on top of this and popping it we get exactly that |
|||
# behavior. It's important to not use the push and pop |
|||
# methods of the actual request context object since that would |
|||
# mean that cleanup handlers are called |
|||
_request_ctx_stack.push(outer_reqctx) |
|||
try: |
|||
yield sess |
|||
finally: |
|||
_request_ctx_stack.pop() |
|||
|
|||
resp = app.response_class() |
|||
if not app.session_interface.is_null_session(sess): |
|||
app.save_session(sess, resp) |
|||
headers = resp.get_wsgi_headers(c.request.environ) |
|||
self.cookie_jar.extract_wsgi(c.request.environ, headers) |
|||
|
|||
def open(self, *args, **kwargs): |
|||
kwargs.setdefault('environ_overrides', {}) \ |
|||
['flask._preserve_context'] = self.preserve_context |
|||
|
|||
as_tuple = kwargs.pop('as_tuple', False) |
|||
buffered = kwargs.pop('buffered', False) |
|||
follow_redirects = kwargs.pop('follow_redirects', False) |
|||
builder = make_test_environ_builder(self.application, *args, **kwargs) |
|||
|
|||
return Client.open(self, builder, |
|||
as_tuple=as_tuple, |
|||
buffered=buffered, |
|||
follow_redirects=follow_redirects) |
|||
|
|||
def __enter__(self): |
|||
if self.preserve_context: |
|||
raise RuntimeError('Cannot nest client invocations') |
|||
self.preserve_context = True |
|||
return self |
|||
|
|||
def __exit__(self, exc_type, exc_value, tb): |
|||
self.preserve_context = False |
|||
|
|||
# on exit we want to clean up earlier. Normally the request context |
|||
# stays preserved until the next request in the same thread comes |
|||
# in. See RequestGlobals.push() for the general behavior. |
|||
top = _request_ctx_stack.top |
|||
if top is not None and top.preserved: |
|||
top.pop() |
@ -1,150 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
flask.views |
|||
~~~~~~~~~~~ |
|||
|
|||
This module provides class-based views inspired by the ones in Django. |
|||
|
|||
:copyright: (c) 2011 by Armin Ronacher. |
|||
:license: BSD, see LICENSE for more details. |
|||
""" |
|||
from .globals import request |
|||
|
|||
|
|||
http_method_funcs = frozenset(['get', 'post', 'head', 'options', |
|||
'delete', 'put', 'trace', 'patch']) |
|||
|
|||
|
|||
class View(object): |
|||
"""Alternative way to use view functions. A subclass has to implement |
|||
:meth:`dispatch_request` which is called with the view arguments from |
|||
the URL routing system. If :attr:`methods` is provided the methods |
|||
do not have to be passed to the :meth:`~flask.Flask.add_url_rule` |
|||
method explicitly:: |
|||
|
|||
class MyView(View): |
|||
methods = ['GET'] |
|||
|
|||
def dispatch_request(self, name): |
|||
return 'Hello %s!' % name |
|||
|
|||
app.add_url_rule('/hello/<name>', view_func=MyView.as_view('myview')) |
|||
|
|||
When you want to decorate a pluggable view you will have to either do that |
|||
when the view function is created (by wrapping the return value of |
|||
:meth:`as_view`) or you can use the :attr:`decorators` attribute:: |
|||
|
|||
class SecretView(View): |
|||
methods = ['GET'] |
|||
decorators = [superuser_required] |
|||
|
|||
def dispatch_request(self): |
|||
... |
|||
|
|||
The decorators stored in the decorators list are applied one after another |
|||
when the view function is created. Note that you can *not* use the class |
|||
based decorators since those would decorate the view class and not the |
|||
generated view function! |
|||
""" |
|||
|
|||
#: A for which methods this pluggable view can handle. |
|||
methods = None |
|||
|
|||
#: The canonical way to decorate class-based views is to decorate the |
|||
#: return value of as_view(). However since this moves parts of the |
|||
#: logic from the class declaration to the place where it's hooked |
|||
#: into the routing system. |
|||
#: |
|||
#: You can place one or more decorators in this list and whenever the |
|||
#: view function is created the result is automatically decorated. |
|||
#: |
|||
#: .. versionadded:: 0.8 |
|||
decorators = [] |
|||
|
|||
def dispatch_request(self): |
|||
"""Subclasses have to override this method to implement the |
|||
actual view function code. This method is called with all |
|||
the arguments from the URL rule. |
|||
""" |
|||
raise NotImplementedError() |
|||
|
|||
@classmethod |
|||
def as_view(cls, name, *class_args, **class_kwargs): |
|||
"""Converts the class into an actual view function that can be used |
|||
with the routing system. Internally this generates a function on the |
|||
fly which will instantiate the :class:`View` on each request and call |
|||
the :meth:`dispatch_request` method on it. |
|||
|
|||
The arguments passed to :meth:`as_view` are forwarded to the |
|||
constructor of the class. |
|||
""" |
|||
def view(*args, **kwargs): |
|||
self = view.view_class(*class_args, **class_kwargs) |
|||
return self.dispatch_request(*args, **kwargs) |
|||
|
|||
if cls.decorators: |
|||
view.__name__ = name |
|||
view.__module__ = cls.__module__ |
|||
for decorator in cls.decorators: |
|||
view = decorator(view) |
|||
|
|||
# we attach the view class to the view function for two reasons: |
|||
# first of all it allows us to easily figure out what class-based |
|||
# view this thing came from, secondly it's also used for instantiating |
|||
# the view class so you can actually replace it with something else |
|||
# for testing purposes and debugging. |
|||
view.view_class = cls |
|||
view.__name__ = name |
|||
view.__doc__ = cls.__doc__ |
|||
view.__module__ = cls.__module__ |
|||
view.methods = cls.methods |
|||
return view |
|||
|
|||
|
|||
class MethodViewType(type): |
|||
|
|||
def __new__(cls, name, bases, d): |
|||
rv = type.__new__(cls, name, bases, d) |
|||
if 'methods' not in d: |
|||
methods = set(rv.methods or []) |
|||
for key in d: |
|||
if key in http_method_funcs: |
|||
methods.add(key.upper()) |
|||
# if we have no method at all in there we don't want to |
|||
# add a method list. (This is for instance the case for |
|||
# the baseclass or another subclass of a base method view |
|||
# that does not introduce new methods). |
|||
if methods: |
|||
rv.methods = sorted(methods) |
|||
return rv |
|||
|
|||
|
|||
class MethodView(View): |
|||
"""Like a regular class-based view but that dispatches requests to |
|||
particular methods. For instance if you implement a method called |
|||
:meth:`get` it means you will response to ``'GET'`` requests and |
|||
the :meth:`dispatch_request` implementation will automatically |
|||
forward your request to that. Also :attr:`options` is set for you |
|||
automatically:: |
|||
|
|||
class CounterAPI(MethodView): |
|||
|
|||
def get(self): |
|||
return session.get('counter', 0) |
|||
|
|||
def post(self): |
|||
session['counter'] = session.get('counter', 0) + 1 |
|||
return 'OK' |
|||
|
|||
app.add_url_rule('/counter', view_func=CounterAPI.as_view('counter')) |
|||
""" |
|||
__metaclass__ = MethodViewType |
|||
|
|||
def dispatch_request(self, *args, **kwargs): |
|||
meth = getattr(self, request.method.lower(), None) |
|||
# if the request method is HEAD and we don't have a handler for it |
|||
# retry with GET |
|||
if meth is None and request.method == 'HEAD': |
|||
meth = getattr(self, 'get', None) |
|||
assert meth is not None, 'Unimplemented method %r' % request.method |
|||
return meth(*args, **kwargs) |
@ -1,148 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
flask.wrappers |
|||
~~~~~~~~~~~~~~ |
|||
|
|||
Implements the WSGI wrappers (request and response). |
|||
|
|||
:copyright: (c) 2011 by Armin Ronacher. |
|||
:license: BSD, see LICENSE for more details. |
|||
""" |
|||
|
|||
from werkzeug.wrappers import Request as RequestBase, Response as ResponseBase |
|||
from werkzeug.utils import cached_property |
|||
|
|||
from .exceptions import JSONBadRequest |
|||
from .debughelpers import attach_enctype_error_multidict |
|||
from .helpers import json, _assert_have_json |
|||
from .globals import _request_ctx_stack |
|||
|
|||
|
|||
class Request(RequestBase): |
|||
"""The request object used by default in Flask. Remembers the |
|||
matched endpoint and view arguments. |
|||
|
|||
It is what ends up as :class:`~flask.request`. If you want to replace |
|||
the request object used you can subclass this and set |
|||
:attr:`~flask.Flask.request_class` to your subclass. |
|||
|
|||
The request object is a :class:`~werkzeug.wrappers.Request` subclass and |
|||
provides all of the attributes Werkzeug defines plus a few Flask |
|||
specific ones. |
|||
""" |
|||
|
|||
#: the internal URL rule that matched the request. This can be |
|||
#: useful to inspect which methods are allowed for the URL from |
|||
#: a before/after handler (``request.url_rule.methods``) etc. |
|||
#: |
|||
#: .. versionadded:: 0.6 |
|||
url_rule = None |
|||
|
|||
#: a dict of view arguments that matched the request. If an exception |
|||
#: happened when matching, this will be `None`. |
|||
view_args = None |
|||
|
|||
#: if matching the URL failed, this is the exception that will be |
|||
#: raised / was raised as part of the request handling. This is |
|||
#: usually a :exc:`~werkzeug.exceptions.NotFound` exception or |
|||
#: something similar. |
|||
routing_exception = None |
|||
|
|||
# switched by the request context until 1.0 to opt in deprecated |
|||
# module functionality |
|||
_is_old_module = False |
|||
|
|||
@property |
|||
def max_content_length(self): |
|||
"""Read-only view of the `MAX_CONTENT_LENGTH` config key.""" |
|||
ctx = _request_ctx_stack.top |
|||
if ctx is not None: |
|||
return ctx.app.config['MAX_CONTENT_LENGTH'] |
|||
|
|||
@property |
|||
def endpoint(self): |
|||
"""The endpoint that matched the request. This in combination with |
|||
:attr:`view_args` can be used to reconstruct the same or a |
|||
modified URL. If an exception happened when matching, this will |
|||
be `None`. |
|||
""" |
|||
if self.url_rule is not None: |
|||
return self.url_rule.endpoint |
|||
|
|||
@property |
|||
def module(self): |
|||
"""The name of the current module if the request was dispatched |
|||
to an actual module. This is deprecated functionality, use blueprints |
|||
instead. |
|||
""" |
|||
from warnings import warn |
|||
warn(DeprecationWarning('modules were deprecated in favor of ' |
|||
'blueprints. Use request.blueprint ' |
|||
'instead.'), stacklevel=2) |
|||
if self._is_old_module: |
|||
return self.blueprint |
|||
|
|||
@property |
|||
def blueprint(self): |
|||
"""The name of the current blueprint""" |
|||
if self.url_rule and '.' in self.url_rule.endpoint: |
|||
return self.url_rule.endpoint.rsplit('.', 1)[0] |
|||
|
|||
@cached_property |
|||
def json(self): |
|||
"""If the mimetype is `application/json` this will contain the |
|||
parsed JSON data. Otherwise this will be `None`. |
|||
|
|||
This requires Python 2.6 or an installed version of simplejson. |
|||
""" |
|||
if __debug__: |
|||
_assert_have_json() |
|||
if self.mimetype == 'application/json': |
|||
request_charset = self.mimetype_params.get('charset') |
|||
try: |
|||
if request_charset is not None: |
|||
return json.loads(self.data, encoding=request_charset) |
|||
return json.loads(self.data) |
|||
except ValueError, e: |
|||
return self.on_json_loading_failed(e) |
|||
|
|||
def on_json_loading_failed(self, e): |
|||
"""Called if decoding of the JSON data failed. The return value of |
|||
this method is used by :attr:`json` when an error ocurred. The default |
|||
implementation raises a :class:`JSONBadRequest`, which is a subclass of |
|||
:class:`~werkzeug.exceptions.BadRequest` which sets the |
|||
``Content-Type`` to ``application/json`` and provides a JSON-formatted |
|||
error description:: |
|||
|
|||
{"description": "The browser (or proxy) sent a request that \ |
|||
this server could not understand."} |
|||
|
|||
.. versionchanged:: 0.9 |
|||
Return a :class:`JSONBadRequest` instead of a |
|||
:class:`~werkzeug.exceptions.BadRequest` by default. |
|||
|
|||
.. versionadded:: 0.8 |
|||
""" |
|||
raise JSONBadRequest() |
|||
|
|||
def _load_form_data(self): |
|||
RequestBase._load_form_data(self) |
|||
|
|||
# in debug mode we're replacing the files multidict with an ad-hoc |
|||
# subclass that raises a different error for key errors. |
|||
ctx = _request_ctx_stack.top |
|||
if ctx is not None and ctx.app.debug and \ |
|||
self.mimetype != 'multipart/form-data' and not self.files: |
|||
attach_enctype_error_multidict(self) |
|||
|
|||
|
|||
class Response(ResponseBase): |
|||
"""The response object that is used by default in Flask. Works like the |
|||
response object from Werkzeug but is set to have an HTML mimetype by |
|||
default. Quite often you don't have to create this object yourself because |
|||
:meth:`~flask.Flask.make_response` will take care of that for you. |
|||
|
|||
If you want to replace the response object used you can subclass this and |
|||
set :attr:`~flask.Flask.response_class` to your subclass. |
|||
""" |
|||
default_mimetype = 'text/html' |
@ -1,69 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
jinja2 |
|||
~~~~~~ |
|||
|
|||
Jinja2 is a template engine written in pure Python. It provides a |
|||
Django inspired non-XML syntax but supports inline expressions and |
|||
an optional sandboxed environment. |
|||
|
|||
Nutshell |
|||
-------- |
|||
|
|||
Here a small example of a Jinja2 template:: |
|||
|
|||
{% extends 'base.html' %} |
|||
{% block title %}Memberlist{% endblock %} |
|||
{% block content %} |
|||
<ul> |
|||
{% for user in users %} |
|||
<li><a href="{{ user.url }}">{{ user.username }}</a></li> |
|||
{% endfor %} |
|||
</ul> |
|||
{% endblock %} |
|||
|
|||
|
|||
:copyright: (c) 2010 by the Jinja Team. |
|||
:license: BSD, see LICENSE for more details. |
|||
""" |
|||
__docformat__ = 'restructuredtext en' |
|||
__version__ = '2.6' |
|||
|
|||
# high level interface |
|||
from jinja2.environment import Environment, Template |
|||
|
|||
# loaders |
|||
from jinja2.loaders import BaseLoader, FileSystemLoader, PackageLoader, \ |
|||
DictLoader, FunctionLoader, PrefixLoader, ChoiceLoader, \ |
|||
ModuleLoader |
|||
|
|||
# bytecode caches |
|||
from jinja2.bccache import BytecodeCache, FileSystemBytecodeCache, \ |
|||
MemcachedBytecodeCache |
|||
|
|||
# undefined types |
|||
from jinja2.runtime import Undefined, DebugUndefined, StrictUndefined |
|||
|
|||
# exceptions |
|||
from jinja2.exceptions import TemplateError, UndefinedError, \ |
|||
TemplateNotFound, TemplatesNotFound, TemplateSyntaxError, \ |
|||
TemplateAssertionError |
|||
|
|||
# decorators and public utilities |
|||
from jinja2.filters import environmentfilter, contextfilter, \ |
|||
evalcontextfilter |
|||
from jinja2.utils import Markup, escape, clear_caches, \ |
|||
environmentfunction, evalcontextfunction, contextfunction, \ |
|||
is_undefined |
|||
|
|||
__all__ = [ |
|||
'Environment', 'Template', 'BaseLoader', 'FileSystemLoader', |
|||
'PackageLoader', 'DictLoader', 'FunctionLoader', 'PrefixLoader', |
|||
'ChoiceLoader', 'BytecodeCache', 'FileSystemBytecodeCache', |
|||
'MemcachedBytecodeCache', 'Undefined', 'DebugUndefined', |
|||
'StrictUndefined', 'TemplateError', 'UndefinedError', 'TemplateNotFound', |
|||
'TemplatesNotFound', 'TemplateSyntaxError', 'TemplateAssertionError', |
|||
'ModuleLoader', 'environmentfilter', 'contextfilter', 'Markup', 'escape', |
|||
'environmentfunction', 'contextfunction', 'clear_caches', 'is_undefined', |
|||
'evalcontextfilter', 'evalcontextfunction' |
|||
] |
@ -1,78 +0,0 @@ |
|||
/**
|
|||
* jinja2._debugsupport |
|||
* ~~~~~~~~~~~~~~~~~~~~ |
|||
* |
|||
* C implementation of `tb_set_next`. |
|||
* |
|||
* :copyright: (c) 2010 by the Jinja Team. |
|||
* :license: BSD. |
|||
*/ |
|||
|
|||
#include <Python.h> |
|||
|
|||
|
|||
static PyObject* |
|||
tb_set_next(PyObject *self, PyObject *args) |
|||
{ |
|||
PyTracebackObject *tb, *old; |
|||
PyObject *next; |
|||
|
|||
if (!PyArg_ParseTuple(args, "O!O:tb_set_next", &PyTraceBack_Type, &tb, &next)) |
|||
return NULL; |
|||
if (next == Py_None) |
|||
next = NULL; |
|||
else if (!PyTraceBack_Check(next)) { |
|||
PyErr_SetString(PyExc_TypeError, |
|||
"tb_set_next arg 2 must be traceback or None"); |
|||
return NULL; |
|||
} |
|||
else |
|||
Py_INCREF(next); |
|||
|
|||
old = tb->tb_next; |
|||
tb->tb_next = (PyTracebackObject*)next; |
|||
Py_XDECREF(old); |
|||
|
|||
Py_INCREF(Py_None); |
|||
return Py_None; |
|||
} |
|||
|
|||
static PyMethodDef module_methods[] = { |
|||
{"tb_set_next", (PyCFunction)tb_set_next, METH_VARARGS, |
|||
"Set the tb_next member of a traceback object."}, |
|||
{NULL, NULL, 0, NULL} /* Sentinel */ |
|||
}; |
|||
|
|||
|
|||
#if PY_MAJOR_VERSION < 3 |
|||
|
|||
#ifndef PyMODINIT_FUNC /* declarations for DLL import/export */ |
|||
#define PyMODINIT_FUNC void |
|||
#endif |
|||
PyMODINIT_FUNC |
|||
init_debugsupport(void) |
|||
{ |
|||
Py_InitModule3("jinja2._debugsupport", module_methods, ""); |
|||
} |
|||
|
|||
#else /* Python 3.x module initialization */ |
|||
|
|||
static struct PyModuleDef module_definition = { |
|||
PyModuleDef_HEAD_INIT, |
|||
"jinja2._debugsupport", |
|||
NULL, |
|||
-1, |
|||
module_methods, |
|||
NULL, |
|||
NULL, |
|||
NULL, |
|||
NULL |
|||
}; |
|||
|
|||
PyMODINIT_FUNC |
|||
PyInit__debugsupport(void) |
|||
{ |
|||
return PyModule_Create(&module_definition); |
|||
} |
|||
|
|||
#endif |
@ -1,225 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
markupsafe |
|||
~~~~~~~~~~ |
|||
|
|||
Implements a Markup string. |
|||
|
|||
:copyright: (c) 2010 by Armin Ronacher. |
|||
:license: BSD, see LICENSE for more details. |
|||
""" |
|||
import re |
|||
from itertools import imap |
|||
|
|||
|
|||
__all__ = ['Markup', 'soft_unicode', 'escape', 'escape_silent'] |
|||
|
|||
|
|||
_striptags_re = re.compile(r'(<!--.*?-->|<[^>]*>)') |
|||
_entity_re = re.compile(r'&([^;]+);') |
|||
|
|||
|
|||
class Markup(unicode): |
|||
r"""Marks a string as being safe for inclusion in HTML/XML output without |
|||
needing to be escaped. This implements the `__html__` interface a couple |
|||
of frameworks and web applications use. :class:`Markup` is a direct |
|||
subclass of `unicode` and provides all the methods of `unicode` just that |
|||
it escapes arguments passed and always returns `Markup`. |
|||
|
|||
The `escape` function returns markup objects so that double escaping can't |
|||
happen. |
|||
|
|||
The constructor of the :class:`Markup` class can be used for three |
|||
different things: When passed an unicode object it's assumed to be safe, |
|||
when passed an object with an HTML representation (has an `__html__` |
|||
method) that representation is used, otherwise the object passed is |
|||
converted into a unicode string and then assumed to be safe: |
|||
|
|||
>>> Markup("Hello <em>World</em>!") |
|||
Markup(u'Hello <em>World</em>!') |
|||
>>> class Foo(object): |
|||
... def __html__(self): |
|||
... return '<a href="#">foo</a>' |
|||
... |
|||
>>> Markup(Foo()) |
|||
Markup(u'<a href="#">foo</a>') |
|||
|
|||
If you want object passed being always treated as unsafe you can use the |
|||
:meth:`escape` classmethod to create a :class:`Markup` object: |
|||
|
|||
>>> Markup.escape("Hello <em>World</em>!") |
|||
Markup(u'Hello <em>World</em>!') |
|||
|
|||
Operations on a markup string are markup aware which means that all |
|||
arguments are passed through the :func:`escape` function: |
|||
|
|||
>>> em = Markup("<em>%s</em>") |
|||
>>> em % "foo & bar" |
|||
Markup(u'<em>foo & bar</em>') |
|||
>>> strong = Markup("<strong>%(text)s</strong>") |
|||
>>> strong % {'text': '<blink>hacker here</blink>'} |
|||
Markup(u'<strong><blink>hacker here</blink></strong>') |
|||
>>> Markup("<em>Hello</em> ") + "<foo>" |
|||
Markup(u'<em>Hello</em> <foo>') |
|||
""" |
|||
__slots__ = () |
|||
|
|||
def __new__(cls, base=u'', encoding=None, errors='strict'): |
|||
if hasattr(base, '__html__'): |
|||
base = base.__html__() |
|||
if encoding is None: |
|||
return unicode.__new__(cls, base) |
|||
return unicode.__new__(cls, base, encoding, errors) |
|||
|
|||
def __html__(self): |
|||
return self |
|||
|
|||
def __add__(self, other): |
|||
if hasattr(other, '__html__') or isinstance(other, basestring): |
|||
return self.__class__(unicode(self) + unicode(escape(other))) |
|||
return NotImplemented |
|||
|
|||
def __radd__(self, other): |
|||
if hasattr(other, '__html__') or isinstance(other, basestring): |
|||
return self.__class__(unicode(escape(other)) + unicode(self)) |
|||
return NotImplemented |
|||
|
|||
def __mul__(self, num): |
|||
if isinstance(num, (int, long)): |
|||
return self.__class__(unicode.__mul__(self, num)) |
|||
return NotImplemented |
|||
__rmul__ = __mul__ |
|||
|
|||
def __mod__(self, arg): |
|||
if isinstance(arg, tuple): |
|||
arg = tuple(imap(_MarkupEscapeHelper, arg)) |
|||
else: |
|||
arg = _MarkupEscapeHelper(arg) |
|||
return self.__class__(unicode.__mod__(self, arg)) |
|||
|
|||
def __repr__(self): |
|||
return '%s(%s)' % ( |
|||
self.__class__.__name__, |
|||
unicode.__repr__(self) |
|||
) |
|||
|
|||
def join(self, seq): |
|||
return self.__class__(unicode.join(self, imap(escape, seq))) |
|||
join.__doc__ = unicode.join.__doc__ |
|||
|
|||
def split(self, *args, **kwargs): |
|||
return map(self.__class__, unicode.split(self, *args, **kwargs)) |
|||
split.__doc__ = unicode.split.__doc__ |
|||
|
|||
def rsplit(self, *args, **kwargs): |
|||
return map(self.__class__, unicode.rsplit(self, *args, **kwargs)) |
|||
rsplit.__doc__ = unicode.rsplit.__doc__ |
|||
|
|||
def splitlines(self, *args, **kwargs): |
|||
return map(self.__class__, unicode.splitlines(self, *args, **kwargs)) |
|||
splitlines.__doc__ = unicode.splitlines.__doc__ |
|||
|
|||
def unescape(self): |
|||
r"""Unescape markup again into an unicode string. This also resolves |
|||
known HTML4 and XHTML entities: |
|||
|
|||
>>> Markup("Main » <em>About</em>").unescape() |
|||
u'Main \xbb <em>About</em>' |
|||
""" |
|||
from jinja2._markupsafe._constants import HTML_ENTITIES |
|||
def handle_match(m): |
|||
name = m.group(1) |
|||
if name in HTML_ENTITIES: |
|||
return unichr(HTML_ENTITIES[name]) |
|||
try: |
|||
if name[:2] in ('#x', '#X'): |
|||
return unichr(int(name[2:], 16)) |
|||
elif name.startswith('#'): |
|||
return unichr(int(name[1:])) |
|||
except ValueError: |
|||
pass |
|||
return u'' |
|||
return _entity_re.sub(handle_match, unicode(self)) |
|||
|
|||
def striptags(self): |
|||
r"""Unescape markup into an unicode string and strip all tags. This |
|||
also resolves known HTML4 and XHTML entities. Whitespace is |
|||
normalized to one: |
|||
|
|||
>>> Markup("Main » <em>About</em>").striptags() |
|||
u'Main \xbb About' |
|||
""" |
|||
stripped = u' '.join(_striptags_re.sub('', self).split()) |
|||
return Markup(stripped).unescape() |
|||
|
|||
@classmethod |
|||
def escape(cls, s): |
|||
"""Escape the string. Works like :func:`escape` with the difference |
|||
that for subclasses of :class:`Markup` this function would return the |
|||
correct subclass. |
|||
""" |
|||
rv = escape(s) |
|||
if rv.__class__ is not cls: |
|||
return cls(rv) |
|||
return rv |
|||
|
|||
def make_wrapper(name): |
|||
orig = getattr(unicode, name) |
|||
def func(self, *args, **kwargs): |
|||
args = _escape_argspec(list(args), enumerate(args)) |
|||
_escape_argspec(kwargs, kwargs.iteritems()) |
|||
return self.__class__(orig(self, *args, **kwargs)) |
|||
func.__name__ = orig.__name__ |
|||
func.__doc__ = orig.__doc__ |
|||
return func |
|||
|
|||
for method in '__getitem__', 'capitalize', \ |
|||
'title', 'lower', 'upper', 'replace', 'ljust', \ |
|||
'rjust', 'lstrip', 'rstrip', 'center', 'strip', \ |
|||
'translate', 'expandtabs', 'swapcase', 'zfill': |
|||
locals()[method] = make_wrapper(method) |
|||
|
|||
# new in python 2.5 |
|||
if hasattr(unicode, 'partition'): |
|||
partition = make_wrapper('partition'), |
|||
rpartition = make_wrapper('rpartition') |
|||
|
|||
# new in python 2.6 |
|||
if hasattr(unicode, 'format'): |
|||
format = make_wrapper('format') |
|||
|
|||
# not in python 3 |
|||
if hasattr(unicode, '__getslice__'): |
|||
__getslice__ = make_wrapper('__getslice__') |
|||
|
|||
del method, make_wrapper |
|||
|
|||
|
|||
def _escape_argspec(obj, iterable): |
|||
"""Helper for various string-wrapped functions.""" |
|||
for key, value in iterable: |
|||
if hasattr(value, '__html__') or isinstance(value, basestring): |
|||
obj[key] = escape(value) |
|||
return obj |
|||
|
|||
|
|||
class _MarkupEscapeHelper(object): |
|||
"""Helper for Markup.__mod__""" |
|||
|
|||
def __init__(self, obj): |
|||
self.obj = obj |
|||
|
|||
__getitem__ = lambda s, x: _MarkupEscapeHelper(s.obj[x]) |
|||
__str__ = lambda s: str(escape(s.obj)) |
|||
__unicode__ = lambda s: unicode(escape(s.obj)) |
|||
__repr__ = lambda s: str(escape(repr(s.obj))) |
|||
__int__ = lambda s: int(s.obj) |
|||
__float__ = lambda s: float(s.obj) |
|||
|
|||
|
|||
# we have to import it down here as the speedups and native |
|||
# modules imports the markup type which is define above. |
|||
try: |
|||
from jinja2._markupsafe._speedups import escape, escape_silent, soft_unicode |
|||
except ImportError: |
|||
from jinja2._markupsafe._native import escape, escape_silent, soft_unicode |
@ -1,49 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
jinja2._markupsafe._bundle |
|||
~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|||
|
|||
This script pulls in markupsafe from a source folder and |
|||
bundles it with Jinja2. It does not pull in the speedups |
|||
module though. |
|||
|
|||
:copyright: Copyright 2010 by the Jinja team, see AUTHORS. |
|||
:license: BSD, see LICENSE for details. |
|||
""" |
|||
import sys |
|||
import os |
|||
import re |
|||
|
|||
|
|||
def rewrite_imports(lines): |
|||
for idx, line in enumerate(lines): |
|||
new_line = re.sub(r'(import|from)\s+markupsafe\b', |
|||
r'\1 jinja2._markupsafe', line) |
|||
if new_line != line: |
|||
lines[idx] = new_line |
|||
|
|||
|
|||
def main(): |
|||
if len(sys.argv) != 2: |
|||
print 'error: only argument is path to markupsafe' |
|||
sys.exit(1) |
|||
basedir = os.path.dirname(__file__) |
|||
markupdir = sys.argv[1] |
|||
for filename in os.listdir(markupdir): |
|||
if filename.endswith('.py'): |
|||
f = open(os.path.join(markupdir, filename)) |
|||
try: |
|||
lines = list(f) |
|||
finally: |
|||
f.close() |
|||
rewrite_imports(lines) |
|||
f = open(os.path.join(basedir, filename), 'w') |
|||
try: |
|||
for line in lines: |
|||
f.write(line) |
|||
finally: |
|||
f.close() |
|||
|
|||
|
|||
if __name__ == '__main__': |
|||
main() |
@ -1,267 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
markupsafe._constants |
|||
~~~~~~~~~~~~~~~~~~~~~ |
|||
|
|||
Highlevel implementation of the Markup string. |
|||
|
|||
:copyright: (c) 2010 by Armin Ronacher. |
|||
:license: BSD, see LICENSE for more details. |
|||
""" |
|||
|
|||
|
|||
HTML_ENTITIES = { |
|||
'AElig': 198, |
|||
'Aacute': 193, |
|||
'Acirc': 194, |
|||
'Agrave': 192, |
|||
'Alpha': 913, |
|||
'Aring': 197, |
|||
'Atilde': 195, |
|||
'Auml': 196, |
|||
'Beta': 914, |
|||
'Ccedil': 199, |
|||
'Chi': 935, |
|||
'Dagger': 8225, |
|||
'Delta': 916, |
|||
'ETH': 208, |
|||
'Eacute': 201, |
|||
'Ecirc': 202, |
|||
'Egrave': 200, |
|||
'Epsilon': 917, |
|||
'Eta': 919, |
|||
'Euml': 203, |
|||
'Gamma': 915, |
|||
'Iacute': 205, |
|||
'Icirc': 206, |
|||
'Igrave': 204, |
|||
'Iota': 921, |
|||
'Iuml': 207, |
|||
'Kappa': 922, |
|||
'Lambda': 923, |
|||
'Mu': 924, |
|||
'Ntilde': 209, |
|||
'Nu': 925, |
|||
'OElig': 338, |
|||
'Oacute': 211, |
|||
'Ocirc': 212, |
|||
'Ograve': 210, |
|||
'Omega': 937, |
|||
'Omicron': 927, |
|||
'Oslash': 216, |
|||
'Otilde': 213, |
|||
'Ouml': 214, |
|||
'Phi': 934, |
|||
'Pi': 928, |
|||
'Prime': 8243, |
|||
'Psi': 936, |
|||
'Rho': 929, |
|||
'Scaron': 352, |
|||
'Sigma': 931, |
|||
'THORN': 222, |
|||
'Tau': 932, |
|||
'Theta': 920, |
|||
'Uacute': 218, |
|||
'Ucirc': 219, |
|||
'Ugrave': 217, |
|||
'Upsilon': 933, |
|||
'Uuml': 220, |
|||
'Xi': 926, |
|||
'Yacute': 221, |
|||
'Yuml': 376, |
|||
'Zeta': 918, |
|||
'aacute': 225, |
|||
'acirc': 226, |
|||
'acute': 180, |
|||
'aelig': 230, |
|||
'agrave': 224, |
|||
'alefsym': 8501, |
|||
'alpha': 945, |
|||
'amp': 38, |
|||
'and': 8743, |
|||
'ang': 8736, |
|||
'apos': 39, |
|||
'aring': 229, |
|||
'asymp': 8776, |
|||
'atilde': 227, |
|||
'auml': 228, |
|||
'bdquo': 8222, |
|||
'beta': 946, |
|||
'brvbar': 166, |
|||
'bull': 8226, |
|||
'cap': 8745, |
|||
'ccedil': 231, |
|||
'cedil': 184, |
|||
'cent': 162, |
|||
'chi': 967, |
|||
'circ': 710, |
|||
'clubs': 9827, |
|||
'cong': 8773, |
|||
'copy': 169, |
|||
'crarr': 8629, |
|||
'cup': 8746, |
|||
'curren': 164, |
|||
'dArr': 8659, |
|||
'dagger': 8224, |
|||
'darr': 8595, |
|||
'deg': 176, |
|||
'delta': 948, |
|||
'diams': 9830, |
|||
'divide': 247, |
|||
'eacute': 233, |
|||
'ecirc': 234, |
|||
'egrave': 232, |
|||
'empty': 8709, |
|||
'emsp': 8195, |
|||
'ensp': 8194, |
|||
'epsilon': 949, |
|||
'equiv': 8801, |
|||
'eta': 951, |
|||
'eth': 240, |
|||
'euml': 235, |
|||
'euro': 8364, |
|||
'exist': 8707, |
|||
'fnof': 402, |
|||
'forall': 8704, |
|||
'frac12': 189, |
|||
'frac14': 188, |
|||
'frac34': 190, |
|||
'frasl': 8260, |
|||
'gamma': 947, |
|||
'ge': 8805, |
|||
'gt': 62, |
|||
'hArr': 8660, |
|||
'harr': 8596, |
|||
'hearts': 9829, |
|||
'hellip': 8230, |
|||
'iacute': 237, |
|||
'icirc': 238, |
|||
'iexcl': 161, |
|||
'igrave': 236, |
|||
'image': 8465, |
|||
'infin': 8734, |
|||
'int': 8747, |
|||
'iota': 953, |
|||
'iquest': 191, |
|||
'isin': 8712, |
|||
'iuml': 239, |
|||
'kappa': 954, |
|||
'lArr': 8656, |
|||
'lambda': 955, |
|||
'lang': 9001, |
|||
'laquo': 171, |
|||
'larr': 8592, |
|||
'lceil': 8968, |
|||
'ldquo': 8220, |
|||
'le': 8804, |
|||
'lfloor': 8970, |
|||
'lowast': 8727, |
|||
'loz': 9674, |
|||
'lrm': 8206, |
|||
'lsaquo': 8249, |
|||
'lsquo': 8216, |
|||
'lt': 60, |
|||
'macr': 175, |
|||
'mdash': 8212, |
|||
'micro': 181, |
|||
'middot': 183, |
|||
'minus': 8722, |
|||
'mu': 956, |
|||
'nabla': 8711, |
|||
'nbsp': 160, |
|||
'ndash': 8211, |
|||
'ne': 8800, |
|||
'ni': 8715, |
|||
'not': 172, |
|||
'notin': 8713, |
|||
'nsub': 8836, |
|||
'ntilde': 241, |
|||
'nu': 957, |
|||
'oacute': 243, |
|||
'ocirc': 244, |
|||
'oelig': 339, |
|||
'ograve': 242, |
|||
'oline': 8254, |
|||
'omega': 969, |
|||
'omicron': 959, |
|||
'oplus': 8853, |
|||
'or': 8744, |
|||
'ordf': 170, |
|||
'ordm': 186, |
|||
'oslash': 248, |
|||
'otilde': 245, |
|||
'otimes': 8855, |
|||
'ouml': 246, |
|||
'para': 182, |
|||
'part': 8706, |
|||
'permil': 8240, |
|||
'perp': 8869, |
|||
'phi': 966, |
|||
'pi': 960, |
|||
'piv': 982, |
|||
'plusmn': 177, |
|||
'pound': 163, |
|||
'prime': 8242, |
|||
'prod': 8719, |
|||
'prop': 8733, |
|||
'psi': 968, |
|||
'quot': 34, |
|||
'rArr': 8658, |
|||
'radic': 8730, |
|||
'rang': 9002, |
|||
'raquo': 187, |
|||
'rarr': 8594, |
|||
'rceil': 8969, |
|||
'rdquo': 8221, |
|||
'real': 8476, |
|||
'reg': 174, |
|||
'rfloor': 8971, |
|||
'rho': 961, |
|||
'rlm': 8207, |
|||
'rsaquo': 8250, |
|||
'rsquo': 8217, |
|||
'sbquo': 8218, |
|||
'scaron': 353, |
|||
'sdot': 8901, |
|||
'sect': 167, |
|||
'shy': 173, |
|||
'sigma': 963, |
|||
'sigmaf': 962, |
|||
'sim': 8764, |
|||
'spades': 9824, |
|||
'sub': 8834, |
|||
'sube': 8838, |
|||
'sum': 8721, |
|||
'sup': 8835, |
|||
'sup1': 185, |
|||
'sup2': 178, |
|||
'sup3': 179, |
|||
'supe': 8839, |
|||
'szlig': 223, |
|||
'tau': 964, |
|||
'there4': 8756, |
|||
'theta': 952, |
|||
'thetasym': 977, |
|||
'thinsp': 8201, |
|||
'thorn': 254, |
|||
'tilde': 732, |
|||
'times': 215, |
|||
'trade': 8482, |
|||
'uArr': 8657, |
|||
'uacute': 250, |
|||
'uarr': 8593, |
|||
'ucirc': 251, |
|||
'ugrave': 249, |
|||
'uml': 168, |
|||
'upsih': 978, |
|||
'upsilon': 965, |
|||
'uuml': 252, |
|||
'weierp': 8472, |
|||
'xi': 958, |
|||
'yacute': 253, |
|||
'yen': 165, |
|||
'yuml': 255, |
|||
'zeta': 950, |
|||
'zwj': 8205, |
|||
'zwnj': 8204 |
|||
} |
@ -1,45 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
markupsafe._native |
|||
~~~~~~~~~~~~~~~~~~ |
|||
|
|||
Native Python implementation the C module is not compiled. |
|||
|
|||
:copyright: (c) 2010 by Armin Ronacher. |
|||
:license: BSD, see LICENSE for more details. |
|||
""" |
|||
from jinja2._markupsafe import Markup |
|||
|
|||
|
|||
def escape(s): |
|||
"""Convert the characters &, <, >, ' and " in string s to HTML-safe |
|||
sequences. Use this if you need to display text that might contain |
|||
such characters in HTML. Marks return value as markup string. |
|||
""" |
|||
if hasattr(s, '__html__'): |
|||
return s.__html__() |
|||
return Markup(unicode(s) |
|||
.replace('&', '&') |
|||
.replace('>', '>') |
|||
.replace('<', '<') |
|||
.replace("'", ''') |
|||
.replace('"', '"') |
|||
) |
|||
|
|||
|
|||
def escape_silent(s): |
|||
"""Like :func:`escape` but converts `None` into an empty |
|||
markup string. |
|||
""" |
|||
if s is None: |
|||
return Markup() |
|||
return escape(s) |
|||
|
|||
|
|||
def soft_unicode(s): |
|||
"""Make a string unicode if it isn't already. That way a markup |
|||
string is not converted back to unicode. |
|||
""" |
|||
if not isinstance(s, unicode): |
|||
s = unicode(s) |
|||
return s |
@ -1,80 +0,0 @@ |
|||
import gc |
|||
import unittest |
|||
from jinja2._markupsafe import Markup, escape, escape_silent |
|||
|
|||
|
|||
class MarkupTestCase(unittest.TestCase): |
|||
|
|||
def test_markup_operations(self): |
|||
# adding two strings should escape the unsafe one |
|||
unsafe = '<script type="application/x-some-script">alert("foo");</script>' |
|||
safe = Markup('<em>username</em>') |
|||
assert unsafe + safe == unicode(escape(unsafe)) + unicode(safe) |
|||
|
|||
# string interpolations are safe to use too |
|||
assert Markup('<em>%s</em>') % '<bad user>' == \ |
|||
'<em><bad user></em>' |
|||
assert Markup('<em>%(username)s</em>') % { |
|||
'username': '<bad user>' |
|||
} == '<em><bad user></em>' |
|||
|
|||
# an escaped object is markup too |
|||
assert type(Markup('foo') + 'bar') is Markup |
|||
|
|||
# and it implements __html__ by returning itself |
|||
x = Markup("foo") |
|||
assert x.__html__() is x |
|||
|
|||
# it also knows how to treat __html__ objects |
|||
class Foo(object): |
|||
def __html__(self): |
|||
return '<em>awesome</em>' |
|||
def __unicode__(self): |
|||
return 'awesome' |
|||
assert Markup(Foo()) == '<em>awesome</em>' |
|||
assert Markup('<strong>%s</strong>') % Foo() == \ |
|||
'<strong><em>awesome</em></strong>' |
|||
|
|||
# escaping and unescaping |
|||
assert escape('"<>&\'') == '"<>&'' |
|||
assert Markup("<em>Foo & Bar</em>").striptags() == "Foo & Bar" |
|||
assert Markup("<test>").unescape() == "<test>" |
|||
|
|||
def test_all_set(self): |
|||
import jinja2._markupsafe as markup |
|||
for item in markup.__all__: |
|||
getattr(markup, item) |
|||
|
|||
def test_escape_silent(self): |
|||
assert escape_silent(None) == Markup() |
|||
assert escape(None) == Markup(None) |
|||
assert escape_silent('<foo>') == Markup(u'<foo>') |
|||
|
|||
|
|||
class MarkupLeakTestCase(unittest.TestCase): |
|||
|
|||
def test_markup_leaks(self): |
|||
counts = set() |
|||
for count in xrange(20): |
|||
for item in xrange(1000): |
|||
escape("foo") |
|||
escape("<foo>") |
|||
escape(u"foo") |
|||
escape(u"<foo>") |
|||
counts.add(len(gc.get_objects())) |
|||
assert len(counts) == 1, 'ouch, c extension seems to leak objects' |
|||
|
|||
|
|||
def suite(): |
|||
suite = unittest.TestSuite() |
|||
suite.addTest(unittest.makeSuite(MarkupTestCase)) |
|||
|
|||
# this test only tests the c extension |
|||
if not hasattr(escape, 'func_code'): |
|||
suite.addTest(unittest.makeSuite(MarkupLeakTestCase)) |
|||
|
|||
return suite |
|||
|
|||
|
|||
if __name__ == '__main__': |
|||
unittest.main(defaultTest='suite') |
File diff suppressed because one or more lines are too long
@ -1,301 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
jinja2.bccache |
|||
~~~~~~~~~~~~~~ |
|||
|
|||
This module implements the bytecode cache system Jinja is optionally |
|||
using. This is useful if you have very complex template situations and |
|||
the compiliation of all those templates slow down your application too |
|||
much. |
|||
|
|||
Situations where this is useful are often forking web applications that |
|||
are initialized on the first request. |
|||
|
|||
:copyright: (c) 2010 by the Jinja Team. |
|||
:license: BSD. |
|||
""" |
|||
from os import path, listdir |
|||
import sys |
|||
import marshal |
|||
import tempfile |
|||
import cPickle as pickle |
|||
import fnmatch |
|||
try: |
|||
from hashlib import sha1 |
|||
except ImportError: |
|||
from sha import new as sha1 |
|||
from jinja2.utils import open_if_exists |
|||
|
|||
|
|||
# marshal works better on 3.x, one hack less required |
|||
if sys.version_info > (3, 0): |
|||
from io import BytesIO |
|||
marshal_dump = marshal.dump |
|||
marshal_load = marshal.load |
|||
else: |
|||
from cStringIO import StringIO as BytesIO |
|||
|
|||
def marshal_dump(code, f): |
|||
if isinstance(f, file): |
|||
marshal.dump(code, f) |
|||
else: |
|||
f.write(marshal.dumps(code)) |
|||
|
|||
def marshal_load(f): |
|||
if isinstance(f, file): |
|||
return marshal.load(f) |
|||
return marshal.loads(f.read()) |
|||
|
|||
|
|||
bc_version = 2 |
|||
|
|||
# magic version used to only change with new jinja versions. With 2.6 |
|||
# we change this to also take Python version changes into account. The |
|||
# reason for this is that Python tends to segfault if fed earlier bytecode |
|||
# versions because someone thought it would be a good idea to reuse opcodes |
|||
# or make Python incompatible with earlier versions. |
|||
bc_magic = 'j2'.encode('ascii') + \ |
|||
pickle.dumps(bc_version, 2) + \ |
|||
pickle.dumps((sys.version_info[0] << 24) | sys.version_info[1]) |
|||
|
|||
|
|||
class Bucket(object): |
|||
"""Buckets are used to store the bytecode for one template. It's created |
|||
and initialized by the bytecode cache and passed to the loading functions. |
|||
|
|||
The buckets get an internal checksum from the cache assigned and use this |
|||
to automatically reject outdated cache material. Individual bytecode |
|||
cache subclasses don't have to care about cache invalidation. |
|||
""" |
|||
|
|||
def __init__(self, environment, key, checksum): |
|||
self.environment = environment |
|||
self.key = key |
|||
self.checksum = checksum |
|||
self.reset() |
|||
|
|||
def reset(self): |
|||
"""Resets the bucket (unloads the bytecode).""" |
|||
self.code = None |
|||
|
|||
def load_bytecode(self, f): |
|||
"""Loads bytecode from a file or file like object.""" |
|||
# make sure the magic header is correct |
|||
magic = f.read(len(bc_magic)) |
|||
if magic != bc_magic: |
|||
self.reset() |
|||
return |
|||
# the source code of the file changed, we need to reload |
|||
checksum = pickle.load(f) |
|||
if self.checksum != checksum: |
|||
self.reset() |
|||
return |
|||
self.code = marshal_load(f) |
|||
|
|||
def write_bytecode(self, f): |
|||
"""Dump the bytecode into the file or file like object passed.""" |
|||
if self.code is None: |
|||
raise TypeError('can\'t write empty bucket') |
|||
f.write(bc_magic) |
|||
pickle.dump(self.checksum, f, 2) |
|||
marshal_dump(self.code, f) |
|||
|
|||
def bytecode_from_string(self, string): |
|||
"""Load bytecode from a string.""" |
|||
self.load_bytecode(BytesIO(string)) |
|||
|
|||
def bytecode_to_string(self): |
|||
"""Return the bytecode as string.""" |
|||
out = BytesIO() |
|||
self.write_bytecode(out) |
|||
return out.getvalue() |
|||
|
|||
|
|||
class BytecodeCache(object): |
|||
"""To implement your own bytecode cache you have to subclass this class |
|||
and override :meth:`load_bytecode` and :meth:`dump_bytecode`. Both of |
|||
these methods are passed a :class:`~jinja2.bccache.Bucket`. |
|||
|
|||
A very basic bytecode cache that saves the bytecode on the file system:: |
|||
|
|||
from os import path |
|||
|
|||
class MyCache(BytecodeCache): |
|||
|
|||
def __init__(self, directory): |
|||
self.directory = directory |
|||
|
|||
def load_bytecode(self, bucket): |
|||
filename = path.join(self.directory, bucket.key) |
|||
if path.exists(filename): |
|||
with open(filename, 'rb') as f: |
|||
bucket.load_bytecode(f) |
|||
|
|||
def dump_bytecode(self, bucket): |
|||
filename = path.join(self.directory, bucket.key) |
|||
with open(filename, 'wb') as f: |
|||
bucket.write_bytecode(f) |
|||
|
|||
A more advanced version of a filesystem based bytecode cache is part of |
|||
Jinja2. |
|||
""" |
|||
|
|||
def load_bytecode(self, bucket): |
|||
"""Subclasses have to override this method to load bytecode into a |
|||
bucket. If they are not able to find code in the cache for the |
|||
bucket, it must not do anything. |
|||
""" |
|||
raise NotImplementedError() |
|||
|
|||
def dump_bytecode(self, bucket): |
|||
"""Subclasses have to override this method to write the bytecode |
|||
from a bucket back to the cache. If it unable to do so it must not |
|||
fail silently but raise an exception. |
|||
""" |
|||
raise NotImplementedError() |
|||
|
|||
def clear(self): |
|||
"""Clears the cache. This method is not used by Jinja2 but should be |
|||
implemented to allow applications to clear the bytecode cache used |
|||
by a particular environment. |
|||
""" |
|||
|
|||
def get_cache_key(self, name, filename=None): |
|||
"""Returns the unique hash key for this template name.""" |
|||
hash = sha1(name.encode('utf-8')) |
|||
if filename is not None: |
|||
filename = '|' + filename |
|||
if isinstance(filename, unicode): |
|||
filename = filename.encode('utf-8') |
|||
hash.update(filename) |
|||
return hash.hexdigest() |
|||
|
|||
def get_source_checksum(self, source): |
|||
"""Returns a checksum for the source.""" |
|||
return sha1(source.encode('utf-8')).hexdigest() |
|||
|
|||
def get_bucket(self, environment, name, filename, source): |
|||
"""Return a cache bucket for the given template. All arguments are |
|||
mandatory but filename may be `None`. |
|||
""" |
|||
key = self.get_cache_key(name, filename) |
|||
checksum = self.get_source_checksum(source) |
|||
bucket = Bucket(environment, key, checksum) |
|||
self.load_bytecode(bucket) |
|||
return bucket |
|||
|
|||
def set_bucket(self, bucket): |
|||
"""Put the bucket into the cache.""" |
|||
self.dump_bytecode(bucket) |
|||
|
|||
|
|||
class FileSystemBytecodeCache(BytecodeCache): |
|||
"""A bytecode cache that stores bytecode on the filesystem. It accepts |
|||
two arguments: The directory where the cache items are stored and a |
|||
pattern string that is used to build the filename. |
|||
|
|||
If no directory is specified the system temporary items folder is used. |
|||
|
|||
The pattern can be used to have multiple separate caches operate on the |
|||
same directory. The default pattern is ``'__jinja2_%s.cache'``. ``%s`` |
|||
is replaced with the cache key. |
|||
|
|||
>>> bcc = FileSystemBytecodeCache('/tmp/jinja_cache', '%s.cache') |
|||
|
|||
This bytecode cache supports clearing of the cache using the clear method. |
|||
""" |
|||
|
|||
def __init__(self, directory=None, pattern='__jinja2_%s.cache'): |
|||
if directory is None: |
|||
directory = tempfile.gettempdir() |
|||
self.directory = directory |
|||
self.pattern = pattern |
|||
|
|||
def _get_cache_filename(self, bucket): |
|||
return path.join(self.directory, self.pattern % bucket.key) |
|||
|
|||
def load_bytecode(self, bucket): |
|||
f = open_if_exists(self._get_cache_filename(bucket), 'rb') |
|||
if f is not None: |
|||
try: |
|||
bucket.load_bytecode(f) |
|||
finally: |
|||
f.close() |
|||
|
|||
def dump_bytecode(self, bucket): |
|||
f = open(self._get_cache_filename(bucket), 'wb') |
|||
try: |
|||
bucket.write_bytecode(f) |
|||
finally: |
|||
f.close() |
|||
|
|||
def clear(self): |
|||
# imported lazily here because google app-engine doesn't support |
|||
# write access on the file system and the function does not exist |
|||
# normally. |
|||
from os import remove |
|||
files = fnmatch.filter(listdir(self.directory), self.pattern % '*') |
|||
for filename in files: |
|||
try: |
|||
remove(path.join(self.directory, filename)) |
|||
except OSError: |
|||
pass |
|||
|
|||
|
|||
class MemcachedBytecodeCache(BytecodeCache): |
|||
"""This class implements a bytecode cache that uses a memcache cache for |
|||
storing the information. It does not enforce a specific memcache library |
|||
(tummy's memcache or cmemcache) but will accept any class that provides |
|||
the minimal interface required. |
|||
|
|||
Libraries compatible with this class: |
|||
|
|||
- `werkzeug <http://werkzeug.pocoo.org/>`_.contrib.cache |
|||
- `python-memcached <http://www.tummy.com/Community/software/python-memcached/>`_ |
|||
- `cmemcache <http://gijsbert.org/cmemcache/>`_ |
|||
|
|||
(Unfortunately the django cache interface is not compatible because it |
|||
does not support storing binary data, only unicode. You can however pass |
|||
the underlying cache client to the bytecode cache which is available |
|||
as `django.core.cache.cache._client`.) |
|||
|
|||
The minimal interface for the client passed to the constructor is this: |
|||
|
|||
.. class:: MinimalClientInterface |
|||
|
|||
.. method:: set(key, value[, timeout]) |
|||
|
|||
Stores the bytecode in the cache. `value` is a string and |
|||
`timeout` the timeout of the key. If timeout is not provided |
|||
a default timeout or no timeout should be assumed, if it's |
|||
provided it's an integer with the number of seconds the cache |
|||
item should exist. |
|||
|
|||
.. method:: get(key) |
|||
|
|||
Returns the value for the cache key. If the item does not |
|||
exist in the cache the return value must be `None`. |
|||
|
|||
The other arguments to the constructor are the prefix for all keys that |
|||
is added before the actual cache key and the timeout for the bytecode in |
|||
the cache system. We recommend a high (or no) timeout. |
|||
|
|||
This bytecode cache does not support clearing of used items in the cache. |
|||
The clear method is a no-operation function. |
|||
""" |
|||
|
|||
def __init__(self, client, prefix='jinja2/bytecode/', timeout=None): |
|||
self.client = client |
|||
self.prefix = prefix |
|||
self.timeout = timeout |
|||
|
|||
def load_bytecode(self, bucket): |
|||
code = self.client.get(self.prefix + bucket.key) |
|||
if code is not None: |
|||
bucket.bytecode_from_string(code) |
|||
|
|||
def dump_bytecode(self, bucket): |
|||
args = (self.prefix + bucket.key, bucket.bytecode_to_string()) |
|||
if self.timeout is not None: |
|||
args += (self.timeout,) |
|||
self.client.set(*args) |
File diff suppressed because it is too large
@ -1,32 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
jinja.constants |
|||
~~~~~~~~~~~~~~~ |
|||
|
|||
Various constants. |
|||
|
|||
:copyright: (c) 2010 by the Jinja Team. |
|||
:license: BSD, see LICENSE for more details. |
|||
""" |
|||
|
|||
|
|||
#: list of lorem ipsum words used by the lipsum() helper function |
|||
LOREM_IPSUM_WORDS = u'''\ |
|||
a ac accumsan ad adipiscing aenean aliquam aliquet amet ante aptent arcu at |
|||
auctor augue bibendum blandit class commodo condimentum congue consectetuer |
|||
consequat conubia convallis cras cubilia cum curabitur curae cursus dapibus |
|||
diam dictum dictumst dignissim dis dolor donec dui duis egestas eget eleifend |
|||
elementum elit enim erat eros est et etiam eu euismod facilisi facilisis fames |
|||
faucibus felis fermentum feugiat fringilla fusce gravida habitant habitasse hac |
|||
hendrerit hymenaeos iaculis id imperdiet in inceptos integer interdum ipsum |
|||
justo lacinia lacus laoreet lectus leo libero ligula litora lobortis lorem |
|||
luctus maecenas magna magnis malesuada massa mattis mauris metus mi molestie |
|||
mollis montes morbi mus nam nascetur natoque nec neque netus nibh nisi nisl non |
|||
nonummy nostra nulla nullam nunc odio orci ornare parturient pede pellentesque |
|||
penatibus per pharetra phasellus placerat platea porta porttitor posuere |
|||
potenti praesent pretium primis proin pulvinar purus quam quis quisque rhoncus |
|||
ridiculus risus rutrum sagittis sapien scelerisque sed sem semper senectus sit |
|||
sociis sociosqu sodales sollicitudin suscipit suspendisse taciti tellus tempor |
|||
tempus tincidunt torquent tortor tristique turpis ullamcorper ultrices |
|||
ultricies urna ut varius vehicula vel velit venenatis vestibulum vitae vivamus |
|||
viverra volutpat vulputate''' |
@ -1,339 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
jinja2.debug |
|||
~~~~~~~~~~~~ |
|||
|
|||
Implements the debug interface for Jinja. This module does some pretty |
|||
ugly stuff with the Python traceback system in order to achieve tracebacks |
|||
with correct line numbers, locals and contents. |
|||
|
|||
:copyright: (c) 2010 by the Jinja Team. |
|||
:license: BSD, see LICENSE for more details. |
|||
""" |
|||
import sys |
|||
import traceback |
|||
from types import TracebackType |
|||
from jinja2.utils import CodeType, missing, internal_code |
|||
from jinja2.exceptions import TemplateSyntaxError |
|||
|
|||
# on pypy we can take advantage of transparent proxies |
|||
try: |
|||
from __pypy__ import tproxy |
|||
except ImportError: |
|||
tproxy = None |
|||
|
|||
|
|||
# how does the raise helper look like? |
|||
try: |
|||
exec "raise TypeError, 'foo'" |
|||
except SyntaxError: |
|||
raise_helper = 'raise __jinja_exception__[1]' |
|||
except TypeError: |
|||
raise_helper = 'raise __jinja_exception__[0], __jinja_exception__[1]' |
|||
|
|||
|
|||
class TracebackFrameProxy(object): |
|||
"""Proxies a traceback frame.""" |
|||
|
|||
def __init__(self, tb): |
|||
self.tb = tb |
|||
self._tb_next = None |
|||
|
|||
@property |
|||
def tb_next(self): |
|||
return self._tb_next |
|||
|
|||
def set_next(self, next): |
|||
if tb_set_next is not None: |
|||
try: |
|||
tb_set_next(self.tb, next and next.tb or None) |
|||
except Exception: |
|||
# this function can fail due to all the hackery it does |
|||
# on various python implementations. We just catch errors |
|||
# down and ignore them if necessary. |
|||
pass |
|||
self._tb_next = next |
|||
|
|||
@property |
|||
def is_jinja_frame(self): |
|||
return '__jinja_template__' in self.tb.tb_frame.f_globals |
|||
|
|||
def __getattr__(self, name): |
|||
return getattr(self.tb, name) |
|||
|
|||
|
|||
def make_frame_proxy(frame): |
|||
proxy = TracebackFrameProxy(frame) |
|||
if tproxy is None: |
|||
return proxy |
|||
def operation_handler(operation, *args, **kwargs): |
|||
if operation in ('__getattribute__', '__getattr__'): |
|||
return getattr(proxy, args[0]) |
|||
elif operation == '__setattr__': |
|||
proxy.__setattr__(*args, **kwargs) |
|||
else: |
|||
return getattr(proxy, operation)(*args, **kwargs) |
|||
return tproxy(TracebackType, operation_handler) |
|||
|
|||
|
|||
class ProcessedTraceback(object): |
|||
"""Holds a Jinja preprocessed traceback for priting or reraising.""" |
|||
|
|||
def __init__(self, exc_type, exc_value, frames): |
|||
assert frames, 'no frames for this traceback?' |
|||
self.exc_type = exc_type |
|||
self.exc_value = exc_value |
|||
self.frames = frames |
|||
|
|||
# newly concatenate the frames (which are proxies) |
|||
prev_tb = None |
|||
for tb in self.frames: |
|||
if prev_tb is not None: |
|||
prev_tb.set_next(tb) |
|||
prev_tb = tb |
|||
prev_tb.set_next(None) |
|||
|
|||
def render_as_text(self, limit=None): |
|||
"""Return a string with the traceback.""" |
|||
lines = traceback.format_exception(self.exc_type, self.exc_value, |
|||
self.frames[0], limit=limit) |
|||
return ''.join(lines).rstrip() |
|||
|
|||
def render_as_html(self, full=False): |
|||
"""Return a unicode string with the traceback as rendered HTML.""" |
|||
from jinja2.debugrenderer import render_traceback |
|||
return u'%s\n\n<!--\n%s\n-->' % ( |
|||
render_traceback(self, full=full), |
|||
self.render_as_text().decode('utf-8', 'replace') |
|||
) |
|||
|
|||
@property |
|||
def is_template_syntax_error(self): |
|||
"""`True` if this is a template syntax error.""" |
|||
return isinstance(self.exc_value, TemplateSyntaxError) |
|||
|
|||
@property |
|||
def exc_info(self): |
|||
"""Exception info tuple with a proxy around the frame objects.""" |
|||
return self.exc_type, self.exc_value, self.frames[0] |
|||
|
|||
@property |
|||
def standard_exc_info(self): |
|||
"""Standard python exc_info for re-raising""" |
|||
tb = self.frames[0] |
|||
# the frame will be an actual traceback (or transparent proxy) if |
|||
# we are on pypy or a python implementation with support for tproxy |
|||
if type(tb) is not TracebackType: |
|||
tb = tb.tb |
|||
return self.exc_type, self.exc_value, tb |
|||
|
|||
|
|||
def make_traceback(exc_info, source_hint=None): |
|||
"""Creates a processed traceback object from the exc_info.""" |
|||
exc_type, exc_value, tb = exc_info |
|||
if isinstance(exc_value, TemplateSyntaxError): |
|||
exc_info = translate_syntax_error(exc_value, source_hint) |
|||
initial_skip = 0 |
|||
else: |
|||
initial_skip = 1 |
|||
return translate_exception(exc_info, initial_skip) |
|||
|
|||
|
|||
def translate_syntax_error(error, source=None): |
|||
"""Rewrites a syntax error to please traceback systems.""" |
|||
error.source = source |
|||
error.translated = True |
|||
exc_info = (error.__class__, error, None) |
|||
filename = error.filename |
|||
if filename is None: |
|||
filename = '<unknown>' |
|||
return fake_exc_info(exc_info, filename, error.lineno) |
|||
|
|||
|
|||
def translate_exception(exc_info, initial_skip=0): |
|||
"""If passed an exc_info it will automatically rewrite the exceptions |
|||
all the way down to the correct line numbers and frames. |
|||
""" |
|||
tb = exc_info[2] |
|||
frames = [] |
|||
|
|||
# skip some internal frames if wanted |
|||
for x in xrange(initial_skip): |
|||
if tb is not None: |
|||
tb = tb.tb_next |
|||
initial_tb = tb |
|||
|
|||
while tb is not None: |
|||
# skip frames decorated with @internalcode. These are internal |
|||
# calls we can't avoid and that are useless in template debugging |
|||
# output. |
|||
if tb.tb_frame.f_code in internal_code: |
|||
tb = tb.tb_next |
|||
continue |
|||
|
|||
# save a reference to the next frame if we override the current |
|||
# one with a faked one. |
|||
next = tb.tb_next |
|||
|
|||
# fake template exceptions |
|||
template = tb.tb_frame.f_globals.get('__jinja_template__') |
|||
if template is not None: |
|||
lineno = template.get_corresponding_lineno(tb.tb_lineno) |
|||
tb = fake_exc_info(exc_info[:2] + (tb,), template.filename, |
|||
lineno)[2] |
|||
|
|||
frames.append(make_frame_proxy(tb)) |
|||
tb = next |
|||
|
|||
# if we don't have any exceptions in the frames left, we have to |
|||
# reraise it unchanged. |
|||
# XXX: can we backup here? when could this happen? |
|||
if not frames: |
|||
raise exc_info[0], exc_info[1], exc_info[2] |
|||
|
|||
return ProcessedTraceback(exc_info[0], exc_info[1], frames) |
|||
|
|||
|
|||
def fake_exc_info(exc_info, filename, lineno): |
|||
"""Helper for `translate_exception`.""" |
|||
exc_type, exc_value, tb = exc_info |
|||
|
|||
# figure the real context out |
|||
if tb is not None: |
|||
real_locals = tb.tb_frame.f_locals.copy() |
|||
ctx = real_locals.get('context') |
|||
if ctx: |
|||
locals = ctx.get_all() |
|||
else: |
|||
locals = {} |
|||
for name, value in real_locals.iteritems(): |
|||
if name.startswith('l_') and value is not missing: |
|||
locals[name[2:]] = value |
|||
|
|||
# if there is a local called __jinja_exception__, we get |
|||
# rid of it to not break the debug functionality. |
|||
locals.pop('__jinja_exception__', None) |
|||
else: |
|||
locals = {} |
|||
|
|||
# assamble fake globals we need |
|||
globals = { |
|||
'__name__': filename, |
|||
'__file__': filename, |
|||
'__jinja_exception__': exc_info[:2], |
|||
|
|||
# we don't want to keep the reference to the template around |
|||
# to not cause circular dependencies, but we mark it as Jinja |
|||
# frame for the ProcessedTraceback |
|||
'__jinja_template__': None |
|||
} |
|||
|
|||
# and fake the exception |
|||
code = compile('\n' * (lineno - 1) + raise_helper, filename, 'exec') |
|||
|
|||
# if it's possible, change the name of the code. This won't work |
|||
# on some python environments such as google appengine |
|||
try: |
|||
if tb is None: |
|||
location = 'template' |
|||
else: |
|||
function = tb.tb_frame.f_code.co_name |
|||
if function == 'root': |
|||
location = 'top-level template code' |
|||
elif function.startswith('block_'): |
|||
location = 'block "%s"' % function[6:] |
|||
else: |
|||
location = 'template' |
|||
code = CodeType(0, code.co_nlocals, code.co_stacksize, |
|||
code.co_flags, code.co_code, code.co_consts, |
|||
code.co_names, code.co_varnames, filename, |
|||
location, code.co_firstlineno, |
|||
code.co_lnotab, (), ()) |
|||
except: |
|||
pass |
|||
|
|||
# execute the code and catch the new traceback |
|||
try: |
|||
exec code in globals, locals |
|||
except: |
|||
exc_info = sys.exc_info() |
|||
new_tb = exc_info[2].tb_next |
|||
|
|||
# return without this frame |
|||
return exc_info[:2] + (new_tb,) |
|||
|
|||
|
|||
def _init_ugly_crap(): |
|||
"""This function implements a few ugly things so that we can patch the |
|||
traceback objects. The function returned allows resetting `tb_next` on |
|||
any python traceback object. Do not attempt to use this on non cpython |
|||
interpreters |
|||
""" |
|||
import ctypes |
|||
from types import TracebackType |
|||
|
|||
# figure out side of _Py_ssize_t |
|||
if hasattr(ctypes.pythonapi, 'Py_InitModule4_64'): |
|||
_Py_ssize_t = ctypes.c_int64 |
|||
else: |
|||
_Py_ssize_t = ctypes.c_int |
|||
|
|||
# regular python |
|||
class _PyObject(ctypes.Structure): |
|||
pass |
|||
_PyObject._fields_ = [ |
|||
('ob_refcnt', _Py_ssize_t), |
|||
('ob_type', ctypes.POINTER(_PyObject)) |
|||
] |
|||
|
|||
# python with trace |
|||
if hasattr(sys, 'getobjects'): |
|||
class _PyObject(ctypes.Structure): |
|||
pass |
|||
_PyObject._fields_ = [ |
|||
('_ob_next', ctypes.POINTER(_PyObject)), |
|||
('_ob_prev', ctypes.POINTER(_PyObject)), |
|||
('ob_refcnt', _Py_ssize_t), |
|||
('ob_type', ctypes.POINTER(_PyObject)) |
|||
] |
|||
|
|||
class _Traceback(_PyObject): |
|||
pass |
|||
_Traceback._fields_ = [ |
|||
('tb_next', ctypes.POINTER(_Traceback)), |
|||
('tb_frame', ctypes.POINTER(_PyObject)), |
|||
('tb_lasti', ctypes.c_int), |
|||
('tb_lineno', ctypes.c_int) |
|||
] |
|||
|
|||
def tb_set_next(tb, next): |
|||
"""Set the tb_next attribute of a traceback object.""" |
|||
if not (isinstance(tb, TracebackType) and |
|||
(next is None or isinstance(next, TracebackType))): |
|||
raise TypeError('tb_set_next arguments must be traceback objects') |
|||
obj = _Traceback.from_address(id(tb)) |
|||
if tb.tb_next is not None: |
|||
old = _Traceback.from_address(id(tb.tb_next)) |
|||
old.ob_refcnt -= 1 |
|||
if next is None: |
|||
obj.tb_next = ctypes.POINTER(_Traceback)() |
|||
else: |
|||
next = _Traceback.from_address(id(next)) |
|||
next.ob_refcnt += 1 |
|||
obj.tb_next = ctypes.pointer(next) |
|||
|
|||
return tb_set_next |
|||
|
|||
|
|||
# try to get a tb_set_next implementation if we don't have transparent |
|||
# proxies. |
|||
tb_set_next = None |
|||
if tproxy is None: |
|||
try: |
|||
from jinja2._debugsupport import tb_set_next |
|||
except ImportError: |
|||
try: |
|||
tb_set_next = _init_ugly_crap() |
|||
except: |
|||
pass |
|||
del _init_ugly_crap |
@ -1,40 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
jinja2.defaults |
|||
~~~~~~~~~~~~~~~ |
|||
|
|||
Jinja default filters and tags. |
|||
|
|||
:copyright: (c) 2010 by the Jinja Team. |
|||
:license: BSD, see LICENSE for more details. |
|||
""" |
|||
from jinja2.utils import generate_lorem_ipsum, Cycler, Joiner |
|||
|
|||
|
|||
# defaults for the parser / lexer |
|||
BLOCK_START_STRING = '{%' |
|||
BLOCK_END_STRING = '%}' |
|||
VARIABLE_START_STRING = '{{' |
|||
VARIABLE_END_STRING = '}}' |
|||
COMMENT_START_STRING = '{#' |
|||
COMMENT_END_STRING = '#}' |
|||
LINE_STATEMENT_PREFIX = None |
|||
LINE_COMMENT_PREFIX = None |
|||
TRIM_BLOCKS = False |
|||
NEWLINE_SEQUENCE = '\n' |
|||
|
|||
|
|||
# default filters, tests and namespace |
|||
from jinja2.filters import FILTERS as DEFAULT_FILTERS |
|||
from jinja2.tests import TESTS as DEFAULT_TESTS |
|||
DEFAULT_NAMESPACE = { |
|||
'range': xrange, |
|||
'dict': lambda **kw: kw, |
|||
'lipsum': generate_lorem_ipsum, |
|||
'cycler': Cycler, |
|||
'joiner': Joiner |
|||
} |
|||
|
|||
|
|||
# export all constants |
|||
__all__ = tuple(x for x in locals().keys() if x.isupper()) |
File diff suppressed because it is too large
@ -1,143 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
jinja2.exceptions |
|||
~~~~~~~~~~~~~~~~~ |
|||
|
|||
Jinja exceptions. |
|||
|
|||
:copyright: (c) 2010 by the Jinja Team. |
|||
:license: BSD, see LICENSE for more details. |
|||
""" |
|||
|
|||
|
|||
class TemplateError(Exception): |
|||
"""Baseclass for all template errors.""" |
|||
|
|||
def __init__(self, message=None): |
|||
if message is not None: |
|||
message = unicode(message).encode('utf-8') |
|||
Exception.__init__(self, message) |
|||
|
|||
@property |
|||
def message(self): |
|||
if self.args: |
|||
message = self.args[0] |
|||
if message is not None: |
|||
return message.decode('utf-8', 'replace') |
|||
|
|||
|
|||
class TemplateNotFound(IOError, LookupError, TemplateError): |
|||
"""Raised if a template does not exist.""" |
|||
|
|||
# looks weird, but removes the warning descriptor that just |
|||
# bogusly warns us about message being deprecated |
|||
message = None |
|||
|
|||
def __init__(self, name, message=None): |
|||
IOError.__init__(self) |
|||
if message is None: |
|||
message = name |
|||
self.message = message |
|||
self.name = name |
|||
self.templates = [name] |
|||
|
|||
def __str__(self): |
|||
return self.message.encode('utf-8') |
|||
|
|||
# unicode goes after __str__ because we configured 2to3 to rename |
|||
# __unicode__ to __str__. because the 2to3 tree is not designed to |
|||
# remove nodes from it, we leave the above __str__ around and let |
|||
# it override at runtime. |
|||
def __unicode__(self): |
|||
return self.message |
|||
|
|||
|
|||
class TemplatesNotFound(TemplateNotFound): |
|||
"""Like :class:`TemplateNotFound` but raised if multiple templates |
|||
are selected. This is a subclass of :class:`TemplateNotFound` |
|||
exception, so just catching the base exception will catch both. |
|||
|
|||
.. versionadded:: 2.2 |
|||
""" |
|||
|
|||
def __init__(self, names=(), message=None): |
|||
if message is None: |
|||
message = u'non of the templates given were found: ' + \ |
|||
u', '.join(map(unicode, names)) |
|||
TemplateNotFound.__init__(self, names and names[-1] or None, message) |
|||
self.templates = list(names) |
|||
|
|||
|
|||
class TemplateSyntaxError(TemplateError): |
|||
"""Raised to tell the user that there is a problem with the template.""" |
|||
|
|||
def __init__(self, message, lineno, name=None, filename=None): |
|||
TemplateError.__init__(self, message) |
|||
self.lineno = lineno |
|||
self.name = name |
|||
self.filename = filename |
|||
self.source = None |
|||
|
|||
# this is set to True if the debug.translate_syntax_error |
|||
# function translated the syntax error into a new traceback |
|||
self.translated = False |
|||
|
|||
def __str__(self): |
|||
return unicode(self).encode('utf-8') |
|||
|
|||
# unicode goes after __str__ because we configured 2to3 to rename |
|||
# __unicode__ to __str__. because the 2to3 tree is not designed to |
|||
# remove nodes from it, we leave the above __str__ around and let |
|||
# it override at runtime. |
|||
def __unicode__(self): |
|||
# for translated errors we only return the message |
|||
if self.translated: |
|||
return self.message |
|||
|
|||
# otherwise attach some stuff |
|||
location = 'line %d' % self.lineno |
|||
name = self.filename or self.name |
|||
if name: |
|||
location = 'File "%s", %s' % (name, location) |
|||
lines = [self.message, ' ' + location] |
|||
|
|||
# if the source is set, add the line to the output |
|||
if self.source is not None: |
|||
try: |
|||
line = self.source.splitlines()[self.lineno - 1] |
|||
except IndexError: |
|||
line = None |
|||
if line: |
|||
lines.append(' ' + line.strip()) |
|||
|
|||
return u'\n'.join(lines) |
|||
|
|||
|
|||
class TemplateAssertionError(TemplateSyntaxError): |
|||
"""Like a template syntax error, but covers cases where something in the |
|||
template caused an error at compile time that wasn't necessarily caused |
|||
by a syntax error. However it's a direct subclass of |
|||
:exc:`TemplateSyntaxError` and has the same attributes. |
|||
""" |
|||
|
|||
|
|||
class TemplateRuntimeError(TemplateError): |
|||
"""A generic runtime error in the template engine. Under some situations |
|||
Jinja may raise this exception. |
|||
""" |
|||
|
|||
|
|||
class UndefinedError(TemplateRuntimeError): |
|||
"""Raised if a template tries to operate on :class:`Undefined`.""" |
|||
|
|||
|
|||
class SecurityError(TemplateRuntimeError): |
|||
"""Raised if a template tries to do something insecure if the |
|||
sandbox is enabled. |
|||
""" |
|||
|
|||
|
|||
class FilterArgumentError(TemplateRuntimeError): |
|||
"""This error is raised if a filter was called with inappropriate |
|||
arguments |
|||
""" |
@ -1,612 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
jinja2.ext |
|||
~~~~~~~~~~ |
|||
|
|||
Jinja extensions allow to add custom tags similar to the way django custom |
|||
tags work. By default two example extensions exist: an i18n and a cache |
|||
extension. |
|||
|
|||
:copyright: (c) 2010 by the Jinja Team. |
|||
:license: BSD. |
|||
""" |
|||
from collections import deque |
|||
from jinja2 import nodes |
|||
from jinja2.defaults import * |
|||
from jinja2.environment import Environment |
|||
from jinja2.runtime import Undefined, concat |
|||
from jinja2.exceptions import TemplateAssertionError, TemplateSyntaxError |
|||
from jinja2.utils import contextfunction, import_string, Markup, next |
|||
|
|||
|
|||
# the only real useful gettext functions for a Jinja template. Note |
|||
# that ugettext must be assigned to gettext as Jinja doesn't support |
|||
# non unicode strings. |
|||
GETTEXT_FUNCTIONS = ('_', 'gettext', 'ngettext') |
|||
|
|||
|
|||
class ExtensionRegistry(type): |
|||
"""Gives the extension an unique identifier.""" |
|||
|
|||
def __new__(cls, name, bases, d): |
|||
rv = type.__new__(cls, name, bases, d) |
|||
rv.identifier = rv.__module__ + '.' + rv.__name__ |
|||
return rv |
|||
|
|||
|
|||
class Extension(object): |
|||
"""Extensions can be used to add extra functionality to the Jinja template |
|||
system at the parser level. Custom extensions are bound to an environment |
|||
but may not store environment specific data on `self`. The reason for |
|||
this is that an extension can be bound to another environment (for |
|||
overlays) by creating a copy and reassigning the `environment` attribute. |
|||
|
|||
As extensions are created by the environment they cannot accept any |
|||
arguments for configuration. One may want to work around that by using |
|||
a factory function, but that is not possible as extensions are identified |
|||
by their import name. The correct way to configure the extension is |
|||
storing the configuration values on the environment. Because this way the |
|||
environment ends up acting as central configuration storage the |
|||
attributes may clash which is why extensions have to ensure that the names |
|||
they choose for configuration are not too generic. ``prefix`` for example |
|||
is a terrible name, ``fragment_cache_prefix`` on the other hand is a good |
|||
name as includes the name of the extension (fragment cache). |
|||
""" |
|||
__metaclass__ = ExtensionRegistry |
|||
|
|||
#: if this extension parses this is the list of tags it's listening to. |
|||
tags = set() |
|||
|
|||
#: the priority of that extension. This is especially useful for |
|||
#: extensions that preprocess values. A lower value means higher |
|||
#: priority. |
|||
#: |
|||
#: .. versionadded:: 2.4 |
|||
priority = 100 |
|||
|
|||
def __init__(self, environment): |
|||
self.environment = environment |
|||
|
|||
def bind(self, environment): |
|||
"""Create a copy of this extension bound to another environment.""" |
|||
rv = object.__new__(self.__class__) |
|||
rv.__dict__.update(self.__dict__) |
|||
rv.environment = environment |
|||
return rv |
|||
|
|||
def preprocess(self, source, name, filename=None): |
|||
"""This method is called before the actual lexing and can be used to |
|||
preprocess the source. The `filename` is optional. The return value |
|||
must be the preprocessed source. |
|||
""" |
|||
return source |
|||
|
|||
def filter_stream(self, stream): |
|||
"""It's passed a :class:`~jinja2.lexer.TokenStream` that can be used |
|||
to filter tokens returned. This method has to return an iterable of |
|||
:class:`~jinja2.lexer.Token`\s, but it doesn't have to return a |
|||
:class:`~jinja2.lexer.TokenStream`. |
|||
|
|||
In the `ext` folder of the Jinja2 source distribution there is a file |
|||
called `inlinegettext.py` which implements a filter that utilizes this |
|||
method. |
|||
""" |
|||
return stream |
|||
|
|||
def parse(self, parser): |
|||
"""If any of the :attr:`tags` matched this method is called with the |
|||
parser as first argument. The token the parser stream is pointing at |
|||
is the name token that matched. This method has to return one or a |
|||
list of multiple nodes. |
|||
""" |
|||
raise NotImplementedError() |
|||
|
|||
def attr(self, name, lineno=None): |
|||
"""Return an attribute node for the current extension. This is useful |
|||
to pass constants on extensions to generated template code. |
|||
|
|||
:: |
|||
|
|||
self.attr('_my_attribute', lineno=lineno) |
|||
""" |
|||
return nodes.ExtensionAttribute(self.identifier, name, lineno=lineno) |
|||
|
|||
def call_method(self, name, args=None, kwargs=None, dyn_args=None, |
|||
dyn_kwargs=None, lineno=None): |
|||
"""Call a method of the extension. This is a shortcut for |
|||
:meth:`attr` + :class:`jinja2.nodes.Call`. |
|||
""" |
|||
if args is None: |
|||
args = [] |
|||
if kwargs is None: |
|||
kwargs = [] |
|||
return nodes.Call(self.attr(name, lineno=lineno), args, kwargs, |
|||
dyn_args, dyn_kwargs, lineno=lineno) |
|||
|
|||
|
|||
@contextfunction |
|||
def _gettext_alias(__context, *args, **kwargs): |
|||
return __context.call(__context.resolve('gettext'), *args, **kwargs) |
|||
|
|||
|
|||
def _make_new_gettext(func): |
|||
@contextfunction |
|||
def gettext(__context, __string, **variables): |
|||
rv = __context.call(func, __string) |
|||
if __context.eval_ctx.autoescape: |
|||
rv = Markup(rv) |
|||
return rv % variables |
|||
return gettext |
|||
|
|||
|
|||
def _make_new_ngettext(func): |
|||
@contextfunction |
|||
def ngettext(__context, __singular, __plural, __num, **variables): |
|||
variables.setdefault('num', __num) |
|||
rv = __context.call(func, __singular, __plural, __num) |
|||
if __context.eval_ctx.autoescape: |
|||
rv = Markup(rv) |
|||
return rv % variables |
|||
return ngettext |
|||
|
|||
|
|||
class InternationalizationExtension(Extension): |
|||
"""This extension adds gettext support to Jinja2.""" |
|||
tags = set(['trans']) |
|||
|
|||
# TODO: the i18n extension is currently reevaluating values in a few |
|||
# situations. Take this example: |
|||
# {% trans count=something() %}{{ count }} foo{% pluralize |
|||
# %}{{ count }} fooss{% endtrans %} |
|||
# something is called twice here. One time for the gettext value and |
|||
# the other time for the n-parameter of the ngettext function. |
|||
|
|||
def __init__(self, environment): |
|||
Extension.__init__(self, environment) |
|||
environment.globals['_'] = _gettext_alias |
|||
environment.extend( |
|||
install_gettext_translations=self._install, |
|||
install_null_translations=self._install_null, |
|||
install_gettext_callables=self._install_callables, |
|||
uninstall_gettext_translations=self._uninstall, |
|||
extract_translations=self._extract, |
|||
newstyle_gettext=False |
|||
) |
|||
|
|||
def _install(self, translations, newstyle=None): |
|||
gettext = getattr(translations, 'ugettext', None) |
|||
if gettext is None: |
|||
gettext = translations.gettext |
|||
ngettext = getattr(translations, 'ungettext', None) |
|||
if ngettext is None: |
|||
ngettext = translations.ngettext |
|||
self._install_callables(gettext, ngettext, newstyle) |
|||
|
|||
def _install_null(self, newstyle=None): |
|||
self._install_callables( |
|||
lambda x: x, |
|||
lambda s, p, n: (n != 1 and (p,) or (s,))[0], |
|||
newstyle |
|||
) |
|||
|
|||
def _install_callables(self, gettext, ngettext, newstyle=None): |
|||
if newstyle is not None: |
|||
self.environment.newstyle_gettext = newstyle |
|||
if self.environment.newstyle_gettext: |
|||
gettext = _make_new_gettext(gettext) |
|||
ngettext = _make_new_ngettext(ngettext) |
|||
self.environment.globals.update( |
|||
gettext=gettext, |
|||
ngettext=ngettext |
|||
) |
|||
|
|||
def _uninstall(self, translations): |
|||
for key in 'gettext', 'ngettext': |
|||
self.environment.globals.pop(key, None) |
|||
|
|||
def _extract(self, source, gettext_functions=GETTEXT_FUNCTIONS): |
|||
if isinstance(source, basestring): |
|||
source = self.environment.parse(source) |
|||
return extract_from_ast(source, gettext_functions) |
|||
|
|||
def parse(self, parser): |
|||
"""Parse a translatable tag.""" |
|||
lineno = next(parser.stream).lineno |
|||
num_called_num = False |
|||
|
|||
# find all the variables referenced. Additionally a variable can be |
|||
# defined in the body of the trans block too, but this is checked at |
|||
# a later state. |
|||
plural_expr = None |
|||
variables = {} |
|||
while parser.stream.current.type != 'block_end': |
|||
if variables: |
|||
parser.stream.expect('comma') |
|||
|
|||
# skip colon for python compatibility |
|||
if parser.stream.skip_if('colon'): |
|||
break |
|||
|
|||
name = parser.stream.expect('name') |
|||
if name.value in variables: |
|||
parser.fail('translatable variable %r defined twice.' % |
|||
name.value, name.lineno, |
|||
exc=TemplateAssertionError) |
|||
|
|||
# expressions |
|||
if parser.stream.current.type == 'assign': |
|||
next(parser.stream) |
|||
variables[name.value] = var = parser.parse_expression() |
|||
else: |
|||
variables[name.value] = var = nodes.Name(name.value, 'load') |
|||
|
|||
if plural_expr is None: |
|||
plural_expr = var |
|||
num_called_num = name.value == 'num' |
|||
|
|||
parser.stream.expect('block_end') |
|||
|
|||
plural = plural_names = None |
|||
have_plural = False |
|||
referenced = set() |
|||
|
|||
# now parse until endtrans or pluralize |
|||
singular_names, singular = self._parse_block(parser, True) |
|||
if singular_names: |
|||
referenced.update(singular_names) |
|||
if plural_expr is None: |
|||
plural_expr = nodes.Name(singular_names[0], 'load') |
|||
num_called_num = singular_names[0] == 'num' |
|||
|
|||
# if we have a pluralize block, we parse that too |
|||
if parser.stream.current.test('name:pluralize'): |
|||
have_plural = True |
|||
next(parser.stream) |
|||
if parser.stream.current.type != 'block_end': |
|||
name = parser.stream.expect('name') |
|||
if name.value not in variables: |
|||
parser.fail('unknown variable %r for pluralization' % |
|||
name.value, name.lineno, |
|||
exc=TemplateAssertionError) |
|||
plural_expr = variables[name.value] |
|||
num_called_num = name.value == 'num' |
|||
parser.stream.expect('block_end') |
|||
plural_names, plural = self._parse_block(parser, False) |
|||
next(parser.stream) |
|||
referenced.update(plural_names) |
|||
else: |
|||
next(parser.stream) |
|||
|
|||
# register free names as simple name expressions |
|||
for var in referenced: |
|||
if var not in variables: |
|||
variables[var] = nodes.Name(var, 'load') |
|||
|
|||
if not have_plural: |
|||
plural_expr = None |
|||
elif plural_expr is None: |
|||
parser.fail('pluralize without variables', lineno) |
|||
|
|||
node = self._make_node(singular, plural, variables, plural_expr, |
|||
bool(referenced), |
|||
num_called_num and have_plural) |
|||
node.set_lineno(lineno) |
|||
return node |
|||
|
|||
def _parse_block(self, parser, allow_pluralize): |
|||
"""Parse until the next block tag with a given name.""" |
|||
referenced = [] |
|||
buf = [] |
|||
while 1: |
|||
if parser.stream.current.type == 'data': |
|||
buf.append(parser.stream.current.value.replace('%', '%%')) |
|||
next(parser.stream) |
|||
elif parser.stream.current.type == 'variable_begin': |
|||
next(parser.stream) |
|||
name = parser.stream.expect('name').value |
|||
referenced.append(name) |
|||
buf.append('%%(%s)s' % name) |
|||
parser.stream.expect('variable_end') |
|||
elif parser.stream.current.type == 'block_begin': |
|||
next(parser.stream) |
|||
if parser.stream.current.test('name:endtrans'): |
|||
break |
|||
elif parser.stream.current.test('name:pluralize'): |
|||
if allow_pluralize: |
|||
break |
|||
parser.fail('a translatable section can have only one ' |
|||
'pluralize section') |
|||
parser.fail('control structures in translatable sections are ' |
|||
'not allowed') |
|||
elif parser.stream.eos: |
|||
parser.fail('unclosed translation block') |
|||
else: |
|||
assert False, 'internal parser error' |
|||
|
|||
return referenced, concat(buf) |
|||
|
|||
def _make_node(self, singular, plural, variables, plural_expr, |
|||
vars_referenced, num_called_num): |
|||
"""Generates a useful node from the data provided.""" |
|||
# no variables referenced? no need to escape for old style |
|||
# gettext invocations only if there are vars. |
|||
if not vars_referenced and not self.environment.newstyle_gettext: |
|||
singular = singular.replace('%%', '%') |
|||
if plural: |
|||
plural = plural.replace('%%', '%') |
|||
|
|||
# singular only: |
|||
if plural_expr is None: |
|||
gettext = nodes.Name('gettext', 'load') |
|||
node = nodes.Call(gettext, [nodes.Const(singular)], |
|||
[], None, None) |
|||
|
|||
# singular and plural |
|||
else: |
|||
ngettext = nodes.Name('ngettext', 'load') |
|||
node = nodes.Call(ngettext, [ |
|||
nodes.Const(singular), |
|||
nodes.Const(plural), |
|||
plural_expr |
|||
], [], None, None) |
|||
|
|||
# in case newstyle gettext is used, the method is powerful |
|||
# enough to handle the variable expansion and autoescape |
|||
# handling itself |
|||
if self.environment.newstyle_gettext: |
|||
for key, value in variables.iteritems(): |
|||
# the function adds that later anyways in case num was |
|||
# called num, so just skip it. |
|||
if num_called_num and key == 'num': |
|||
continue |
|||
node.kwargs.append(nodes.Keyword(key, value)) |
|||
|
|||
# otherwise do that here |
|||
else: |
|||
# mark the return value as safe if we are in an |
|||
# environment with autoescaping turned on |
|||
node = nodes.MarkSafeIfAutoescape(node) |
|||
if variables: |
|||
node = nodes.Mod(node, nodes.Dict([ |
|||
nodes.Pair(nodes.Const(key), value) |
|||
for key, value in variables.items() |
|||
])) |
|||
return nodes.Output([node]) |
|||
|
|||
|
|||
class ExprStmtExtension(Extension): |
|||
"""Adds a `do` tag to Jinja2 that works like the print statement just |
|||
that it doesn't print the return value. |
|||
""" |
|||
tags = set(['do']) |
|||
|
|||
def parse(self, parser): |
|||
node = nodes.ExprStmt(lineno=next(parser.stream).lineno) |
|||
node.node = parser.parse_tuple() |
|||
return node |
|||
|
|||
|
|||
class LoopControlExtension(Extension): |
|||
"""Adds break and continue to the template engine.""" |
|||
tags = set(['break', 'continue']) |
|||
|
|||
def parse(self, parser): |
|||
token = next(parser.stream) |
|||
if token.value == 'break': |
|||
return nodes.Break(lineno=token.lineno) |
|||
return nodes.Continue(lineno=token.lineno) |
|||
|
|||
|
|||
class WithExtension(Extension): |
|||
"""Adds support for a django-like with block.""" |
|||
tags = set(['with']) |
|||
|
|||
def parse(self, parser): |
|||
node = nodes.Scope(lineno=next(parser.stream).lineno) |
|||
assignments = [] |
|||
while parser.stream.current.type != 'block_end': |
|||
lineno = parser.stream.current.lineno |
|||
if assignments: |
|||
parser.stream.expect('comma') |
|||
target = parser.parse_assign_target() |
|||
parser.stream.expect('assign') |
|||
expr = parser.parse_expression() |
|||
assignments.append(nodes.Assign(target, expr, lineno=lineno)) |
|||
node.body = assignments + \ |
|||
list(parser.parse_statements(('name:endwith',), |
|||
drop_needle=True)) |
|||
return node |
|||
|
|||
|
|||
class AutoEscapeExtension(Extension): |
|||
"""Changes auto escape rules for a scope.""" |
|||
tags = set(['autoescape']) |
|||
|
|||
def parse(self, parser): |
|||
node = nodes.ScopedEvalContextModifier(lineno=next(parser.stream).lineno) |
|||
node.options = [ |
|||
nodes.Keyword('autoescape', parser.parse_expression()) |
|||
] |
|||
node.body = parser.parse_statements(('name:endautoescape',), |
|||
drop_needle=True) |
|||
return nodes.Scope([node]) |
|||
|
|||
|
|||
def extract_from_ast(node, gettext_functions=GETTEXT_FUNCTIONS, |
|||
babel_style=True): |
|||
"""Extract localizable strings from the given template node. Per |
|||
default this function returns matches in babel style that means non string |
|||
parameters as well as keyword arguments are returned as `None`. This |
|||
allows Babel to figure out what you really meant if you are using |
|||
gettext functions that allow keyword arguments for placeholder expansion. |
|||
If you don't want that behavior set the `babel_style` parameter to `False` |
|||
which causes only strings to be returned and parameters are always stored |
|||
in tuples. As a consequence invalid gettext calls (calls without a single |
|||
string parameter or string parameters after non-string parameters) are |
|||
skipped. |
|||
|
|||
This example explains the behavior: |
|||
|
|||
>>> from jinja2 import Environment |
|||
>>> env = Environment() |
|||
>>> node = env.parse('{{ (_("foo"), _(), ngettext("foo", "bar", 42)) }}') |
|||
>>> list(extract_from_ast(node)) |
|||
[(1, '_', 'foo'), (1, '_', ()), (1, 'ngettext', ('foo', 'bar', None))] |
|||
>>> list(extract_from_ast(node, babel_style=False)) |
|||
[(1, '_', ('foo',)), (1, 'ngettext', ('foo', 'bar'))] |
|||
|
|||
For every string found this function yields a ``(lineno, function, |
|||
message)`` tuple, where: |
|||
|
|||
* ``lineno`` is the number of the line on which the string was found, |
|||
* ``function`` is the name of the ``gettext`` function used (if the |
|||
string was extracted from embedded Python code), and |
|||
* ``message`` is the string itself (a ``unicode`` object, or a tuple |
|||
of ``unicode`` objects for functions with multiple string arguments). |
|||
|
|||
This extraction function operates on the AST and is because of that unable |
|||
to extract any comments. For comment support you have to use the babel |
|||
extraction interface or extract comments yourself. |
|||
""" |
|||
for node in node.find_all(nodes.Call): |
|||
if not isinstance(node.node, nodes.Name) or \ |
|||
node.node.name not in gettext_functions: |
|||
continue |
|||
|
|||
strings = [] |
|||
for arg in node.args: |
|||
if isinstance(arg, nodes.Const) and \ |
|||
isinstance(arg.value, basestring): |
|||
strings.append(arg.value) |
|||
else: |
|||
strings.append(None) |
|||
|
|||
for arg in node.kwargs: |
|||
strings.append(None) |
|||
if node.dyn_args is not None: |
|||
strings.append(None) |
|||
if node.dyn_kwargs is not None: |
|||
strings.append(None) |
|||
|
|||
if not babel_style: |
|||
strings = tuple(x for x in strings if x is not None) |
|||
if not strings: |
|||
continue |
|||
else: |
|||
if len(strings) == 1: |
|||
strings = strings[0] |
|||
else: |
|||
strings = tuple(strings) |
|||
yield node.lineno, node.node.name, strings |
|||
|
|||
|
|||
class _CommentFinder(object): |
|||
"""Helper class to find comments in a token stream. Can only |
|||
find comments for gettext calls forwards. Once the comment |
|||
from line 4 is found, a comment for line 1 will not return a |
|||
usable value. |
|||
""" |
|||
|
|||
def __init__(self, tokens, comment_tags): |
|||
self.tokens = tokens |
|||
self.comment_tags = comment_tags |
|||
self.offset = 0 |
|||
self.last_lineno = 0 |
|||
|
|||
def find_backwards(self, offset): |
|||
try: |
|||
for _, token_type, token_value in \ |
|||
reversed(self.tokens[self.offset:offset]): |
|||
if token_type in ('comment', 'linecomment'): |
|||
try: |
|||
prefix, comment = token_value.split(None, 1) |
|||
except ValueError: |
|||
continue |
|||
if prefix in self.comment_tags: |
|||
return [comment.rstrip()] |
|||
return [] |
|||
finally: |
|||
self.offset = offset |
|||
|
|||
def find_comments(self, lineno): |
|||
if not self.comment_tags or self.last_lineno > lineno: |
|||
return [] |
|||
for idx, (token_lineno, _, _) in enumerate(self.tokens[self.offset:]): |
|||
if token_lineno > lineno: |
|||
return self.find_backwards(self.offset + idx) |
|||
return self.find_backwards(len(self.tokens)) |
|||
|
|||
|
|||
def babel_extract(fileobj, keywords, comment_tags, options): |
|||
"""Babel extraction method for Jinja templates. |
|||
|
|||
.. versionchanged:: 2.3 |
|||
Basic support for translation comments was added. If `comment_tags` |
|||
is now set to a list of keywords for extraction, the extractor will |
|||
try to find the best preceeding comment that begins with one of the |
|||
keywords. For best results, make sure to not have more than one |
|||
gettext call in one line of code and the matching comment in the |
|||
same line or the line before. |
|||
|
|||
.. versionchanged:: 2.5.1 |
|||
The `newstyle_gettext` flag can be set to `True` to enable newstyle |
|||
gettext calls. |
|||
|
|||
:param fileobj: the file-like object the messages should be extracted from |
|||
:param keywords: a list of keywords (i.e. function names) that should be |
|||
recognized as translation functions |
|||
:param comment_tags: a list of translator tags to search for and include |
|||
in the results. |
|||
:param options: a dictionary of additional options (optional) |
|||
:return: an iterator over ``(lineno, funcname, message, comments)`` tuples. |
|||
(comments will be empty currently) |
|||
""" |
|||
extensions = set() |
|||
for extension in options.get('extensions', '').split(','): |
|||
extension = extension.strip() |
|||
if not extension: |
|||
continue |
|||
extensions.add(import_string(extension)) |
|||
if InternationalizationExtension not in extensions: |
|||
extensions.add(InternationalizationExtension) |
|||
|
|||
def getbool(options, key, default=False): |
|||
options.get(key, str(default)).lower() in ('1', 'on', 'yes', 'true') |
|||
|
|||
environment = Environment( |
|||
options.get('block_start_string', BLOCK_START_STRING), |
|||
options.get('block_end_string', BLOCK_END_STRING), |
|||
options.get('variable_start_string', VARIABLE_START_STRING), |
|||
options.get('variable_end_string', VARIABLE_END_STRING), |
|||
options.get('comment_start_string', COMMENT_START_STRING), |
|||
options.get('comment_end_string', COMMENT_END_STRING), |
|||
options.get('line_statement_prefix') or LINE_STATEMENT_PREFIX, |
|||
options.get('line_comment_prefix') or LINE_COMMENT_PREFIX, |
|||
getbool(options, 'trim_blocks', TRIM_BLOCKS), |
|||
NEWLINE_SEQUENCE, frozenset(extensions), |
|||
cache_size=0, |
|||
auto_reload=False |
|||
) |
|||
|
|||
if getbool(options, 'newstyle_gettext'): |
|||
environment.newstyle_gettext = True |
|||
|
|||
source = fileobj.read().decode(options.get('encoding', 'utf-8')) |
|||
try: |
|||
node = environment.parse(source) |
|||
tokens = list(environment.lex(environment.preprocess(source))) |
|||
except TemplateSyntaxError, e: |
|||
# skip templates with syntax errors |
|||
return |
|||
|
|||
finder = _CommentFinder(tokens, comment_tags) |
|||
for lineno, func, message in extract_from_ast(node, keywords): |
|||
yield lineno, func, message, finder.find_comments(lineno) |
|||
|
|||
|
|||
#: nicer import names |
|||
i18n = InternationalizationExtension |
|||
do = ExprStmtExtension |
|||
loopcontrols = LoopControlExtension |
|||
with_ = WithExtension |
|||
autoescape = AutoEscapeExtension |
@ -1,801 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
jinja2.filters |
|||
~~~~~~~~~~~~~~ |
|||
|
|||
Bundled jinja filters. |
|||
|
|||
:copyright: (c) 2010 by the Jinja Team. |
|||
:license: BSD, see LICENSE for more details. |
|||
""" |
|||
import re |
|||
import math |
|||
from random import choice |
|||
from operator import itemgetter |
|||
from itertools import imap, groupby |
|||
from jinja2.utils import Markup, escape, pformat, urlize, soft_unicode |
|||
from jinja2.runtime import Undefined |
|||
from jinja2.exceptions import FilterArgumentError, SecurityError |
|||
|
|||
|
|||
_word_re = re.compile(r'\w+(?u)') |
|||
|
|||
|
|||
def contextfilter(f): |
|||
"""Decorator for marking context dependent filters. The current |
|||
:class:`Context` will be passed as first argument. |
|||
""" |
|||
f.contextfilter = True |
|||
return f |
|||
|
|||
|
|||
def evalcontextfilter(f): |
|||
"""Decorator for marking eval-context dependent filters. An eval |
|||
context object is passed as first argument. For more information |
|||
about the eval context, see :ref:`eval-context`. |
|||
|
|||
.. versionadded:: 2.4 |
|||
""" |
|||
f.evalcontextfilter = True |
|||
return f |
|||
|
|||
|
|||
def environmentfilter(f): |
|||
"""Decorator for marking evironment dependent filters. The current |
|||
:class:`Environment` is passed to the filter as first argument. |
|||
""" |
|||
f.environmentfilter = True |
|||
return f |
|||
|
|||
|
|||
def make_attrgetter(environment, attribute): |
|||
"""Returns a callable that looks up the given attribute from a |
|||
passed object with the rules of the environment. Dots are allowed |
|||
to access attributes of attributes. |
|||
""" |
|||
if not isinstance(attribute, basestring) or '.' not in attribute: |
|||
return lambda x: environment.getitem(x, attribute) |
|||
attribute = attribute.split('.') |
|||
def attrgetter(item): |
|||
for part in attribute: |
|||
item = environment.getitem(item, part) |
|||
return item |
|||
return attrgetter |
|||
|
|||
|
|||
def do_forceescape(value): |
|||
"""Enforce HTML escaping. This will probably double escape variables.""" |
|||
if hasattr(value, '__html__'): |
|||
value = value.__html__() |
|||
return escape(unicode(value)) |
|||
|
|||
|
|||
@evalcontextfilter |
|||
def do_replace(eval_ctx, s, old, new, count=None): |
|||
"""Return a copy of the value with all occurrences of a substring |
|||
replaced with a new one. The first argument is the substring |
|||
that should be replaced, the second is the replacement string. |
|||
If the optional third argument ``count`` is given, only the first |
|||
``count`` occurrences are replaced: |
|||
|
|||
.. sourcecode:: jinja |
|||
|
|||
{{ "Hello World"|replace("Hello", "Goodbye") }} |
|||
-> Goodbye World |
|||
|
|||
{{ "aaaaargh"|replace("a", "d'oh, ", 2) }} |
|||
-> d'oh, d'oh, aaargh |
|||
""" |
|||
if count is None: |
|||
count = -1 |
|||
if not eval_ctx.autoescape: |
|||
return unicode(s).replace(unicode(old), unicode(new), count) |
|||
if hasattr(old, '__html__') or hasattr(new, '__html__') and \ |
|||
not hasattr(s, '__html__'): |
|||
s = escape(s) |
|||
else: |
|||
s = soft_unicode(s) |
|||
return s.replace(soft_unicode(old), soft_unicode(new), count) |
|||
|
|||
|
|||
def do_upper(s): |
|||
"""Convert a value to uppercase.""" |
|||
return soft_unicode(s).upper() |
|||
|
|||
|
|||
def do_lower(s): |
|||
"""Convert a value to lowercase.""" |
|||
return soft_unicode(s).lower() |
|||
|
|||
|
|||
@evalcontextfilter |
|||
def do_xmlattr(_eval_ctx, d, autospace=True): |
|||
"""Create an SGML/XML attribute string based on the items in a dict. |
|||
All values that are neither `none` nor `undefined` are automatically |
|||
escaped: |
|||
|
|||
.. sourcecode:: html+jinja |
|||
|
|||
<ul{{ {'class': 'my_list', 'missing': none, |
|||
'id': 'list-%d'|format(variable)}|xmlattr }}> |
|||
... |
|||
</ul> |
|||
|
|||
Results in something like this: |
|||
|
|||
.. sourcecode:: html |
|||
|
|||
<ul class="my_list" id="list-42"> |
|||
... |
|||
</ul> |
|||
|
|||
As you can see it automatically prepends a space in front of the item |
|||
if the filter returned something unless the second parameter is false. |
|||
""" |
|||
rv = u' '.join( |
|||
u'%s="%s"' % (escape(key), escape(value)) |
|||
for key, value in d.iteritems() |
|||
if value is not None and not isinstance(value, Undefined) |
|||
) |
|||
if autospace and rv: |
|||
rv = u' ' + rv |
|||
if _eval_ctx.autoescape: |
|||
rv = Markup(rv) |
|||
return rv |
|||
|
|||
|
|||
def do_capitalize(s): |
|||
"""Capitalize a value. The first character will be uppercase, all others |
|||
lowercase. |
|||
""" |
|||
return soft_unicode(s).capitalize() |
|||
|
|||
|
|||
def do_title(s): |
|||
"""Return a titlecased version of the value. I.e. words will start with |
|||
uppercase letters, all remaining characters are lowercase. |
|||
""" |
|||
return soft_unicode(s).title() |
|||
|
|||
|
|||
def do_dictsort(value, case_sensitive=False, by='key'): |
|||
"""Sort a dict and yield (key, value) pairs. Because python dicts are |
|||
unsorted you may want to use this function to order them by either |
|||
key or value: |
|||
|
|||
.. sourcecode:: jinja |
|||
|
|||
{% for item in mydict|dictsort %} |
|||
sort the dict by key, case insensitive |
|||
|
|||
{% for item in mydict|dicsort(true) %} |
|||
sort the dict by key, case sensitive |
|||
|
|||
{% for item in mydict|dictsort(false, 'value') %} |
|||
sort the dict by key, case insensitive, sorted |
|||
normally and ordered by value. |
|||
""" |
|||
if by == 'key': |
|||
pos = 0 |
|||
elif by == 'value': |
|||
pos = 1 |
|||
else: |
|||
raise FilterArgumentError('You can only sort by either ' |
|||
'"key" or "value"') |
|||
def sort_func(item): |
|||
value = item[pos] |
|||
if isinstance(value, basestring) and not case_sensitive: |
|||
value = value.lower() |
|||
return value |
|||
|
|||
return sorted(value.items(), key=sort_func) |
|||
|
|||
|
|||
@environmentfilter |
|||
def do_sort(environment, value, reverse=False, case_sensitive=False, |
|||
attribute=None): |
|||
"""Sort an iterable. Per default it sorts ascending, if you pass it |
|||
true as first argument it will reverse the sorting. |
|||
|
|||
If the iterable is made of strings the third parameter can be used to |
|||
control the case sensitiveness of the comparison which is disabled by |
|||
default. |
|||
|
|||
.. sourcecode:: jinja |
|||
|
|||
{% for item in iterable|sort %} |
|||
... |
|||
{% endfor %} |
|||
|
|||
It is also possible to sort by an attribute (for example to sort |
|||
by the date of an object) by specifying the `attribute` parameter: |
|||
|
|||
.. sourcecode:: jinja |
|||
|
|||
{% for item in iterable|sort(attribute='date') %} |
|||
... |
|||
{% endfor %} |
|||
|
|||
.. versionchanged:: 2.6 |
|||
The `attribute` parameter was added. |
|||
""" |
|||
if not case_sensitive: |
|||
def sort_func(item): |
|||
if isinstance(item, basestring): |
|||
item = item.lower() |
|||
return item |
|||
else: |
|||
sort_func = None |
|||
if attribute is not None: |
|||
getter = make_attrgetter(environment, attribute) |
|||
def sort_func(item, processor=sort_func or (lambda x: x)): |
|||
return processor(getter(item)) |
|||
return sorted(value, key=sort_func, reverse=reverse) |
|||
|
|||
|
|||
def do_default(value, default_value=u'', boolean=False): |
|||
"""If the value is undefined it will return the passed default value, |
|||
otherwise the value of the variable: |
|||
|
|||
.. sourcecode:: jinja |
|||
|
|||
{{ my_variable|default('my_variable is not defined') }} |
|||
|
|||
This will output the value of ``my_variable`` if the variable was |
|||
defined, otherwise ``'my_variable is not defined'``. If you want |
|||
to use default with variables that evaluate to false you have to |
|||
set the second parameter to `true`: |
|||
|
|||
.. sourcecode:: jinja |
|||
|
|||
{{ ''|default('the string was empty', true) }} |
|||
""" |
|||
if (boolean and not value) or isinstance(value, Undefined): |
|||
return default_value |
|||
return value |
|||
|
|||
|
|||
@evalcontextfilter |
|||
def do_join(eval_ctx, value, d=u'', attribute=None): |
|||
"""Return a string which is the concatenation of the strings in the |
|||
sequence. The separator between elements is an empty string per |
|||
default, you can define it with the optional parameter: |
|||
|
|||
.. sourcecode:: jinja |
|||
|
|||
{{ [1, 2, 3]|join('|') }} |
|||
-> 1|2|3 |
|||
|
|||
{{ [1, 2, 3]|join }} |
|||
-> 123 |
|||
|
|||
It is also possible to join certain attributes of an object: |
|||
|
|||
.. sourcecode:: jinja |
|||
|
|||
{{ users|join(', ', attribute='username') }} |
|||
|
|||
.. versionadded:: 2.6 |
|||
The `attribute` parameter was added. |
|||
""" |
|||
if attribute is not None: |
|||
value = imap(make_attrgetter(eval_ctx.environment, attribute), value) |
|||
|
|||
# no automatic escaping? joining is a lot eaiser then |
|||
if not eval_ctx.autoescape: |
|||
return unicode(d).join(imap(unicode, value)) |
|||
|
|||
# if the delimiter doesn't have an html representation we check |
|||
# if any of the items has. If yes we do a coercion to Markup |
|||
if not hasattr(d, '__html__'): |
|||
value = list(value) |
|||
do_escape = False |
|||
for idx, item in enumerate(value): |
|||
if hasattr(item, '__html__'): |
|||
do_escape = True |
|||
else: |
|||
value[idx] = unicode(item) |
|||
if do_escape: |
|||
d = escape(d) |
|||
else: |
|||
d = unicode(d) |
|||
return d.join(value) |
|||
|
|||
# no html involved, to normal joining |
|||
return soft_unicode(d).join(imap(soft_unicode, value)) |
|||
|
|||
|
|||
def do_center(value, width=80): |
|||
"""Centers the value in a field of a given width.""" |
|||
return unicode(value).center(width) |
|||
|
|||
|
|||
@environmentfilter |
|||
def do_first(environment, seq): |
|||
"""Return the first item of a sequence.""" |
|||
try: |
|||
return iter(seq).next() |
|||
except StopIteration: |
|||
return environment.undefined('No first item, sequence was empty.') |
|||
|
|||
|
|||
@environmentfilter |
|||
def do_last(environment, seq): |
|||
"""Return the last item of a sequence.""" |
|||
try: |
|||
return iter(reversed(seq)).next() |
|||
except StopIteration: |
|||
return environment.undefined('No last item, sequence was empty.') |
|||
|
|||
|
|||
@environmentfilter |
|||
def do_random(environment, seq): |
|||
"""Return a random item from the sequence.""" |
|||
try: |
|||
return choice(seq) |
|||
except IndexError: |
|||
return environment.undefined('No random item, sequence was empty.') |
|||
|
|||
|
|||
def do_filesizeformat(value, binary=False): |
|||
"""Format the value like a 'human-readable' file size (i.e. 13 kB, |
|||
4.1 MB, 102 Bytes, etc). Per default decimal prefixes are used (Mega, |
|||
Giga, etc.), if the second parameter is set to `True` the binary |
|||
prefixes are used (Mebi, Gibi). |
|||
""" |
|||
bytes = float(value) |
|||
base = binary and 1024 or 1000 |
|||
prefixes = [ |
|||
(binary and "KiB" or "kB"), |
|||
(binary and "MiB" or "MB"), |
|||
(binary and "GiB" or "GB"), |
|||
(binary and "TiB" or "TB"), |
|||
(binary and "PiB" or "PB"), |
|||
(binary and "EiB" or "EB"), |
|||
(binary and "ZiB" or "ZB"), |
|||
(binary and "YiB" or "YB") |
|||
] |
|||
if bytes == 1: |
|||
return "1 Byte" |
|||
elif bytes < base: |
|||
return "%d Bytes" % bytes |
|||
else: |
|||
for i, prefix in enumerate(prefixes): |
|||
unit = base * base ** (i + 1) |
|||
if bytes < unit: |
|||
return "%.1f %s" % ((bytes / unit), prefix) |
|||
return "%.1f %s" % ((bytes / unit), prefix) |
|||
|
|||
|
|||
def do_pprint(value, verbose=False): |
|||
"""Pretty print a variable. Useful for debugging. |
|||
|
|||
With Jinja 1.2 onwards you can pass it a parameter. If this parameter |
|||
is truthy the output will be more verbose (this requires `pretty`) |
|||
""" |
|||
return pformat(value, verbose=verbose) |
|||
|
|||
|
|||
@evalcontextfilter |
|||
def do_urlize(eval_ctx, value, trim_url_limit=None, nofollow=False): |
|||
"""Converts URLs in plain text into clickable links. |
|||
|
|||
If you pass the filter an additional integer it will shorten the urls |
|||
to that number. Also a third argument exists that makes the urls |
|||
"nofollow": |
|||
|
|||
.. sourcecode:: jinja |
|||
|
|||
{{ mytext|urlize(40, true) }} |
|||
links are shortened to 40 chars and defined with rel="nofollow" |
|||
""" |
|||
rv = urlize(value, trim_url_limit, nofollow) |
|||
if eval_ctx.autoescape: |
|||
rv = Markup(rv) |
|||
return rv |
|||
|
|||
|
|||
def do_indent(s, width=4, indentfirst=False): |
|||
"""Return a copy of the passed string, each line indented by |
|||
4 spaces. The first line is not indented. If you want to |
|||
change the number of spaces or indent the first line too |
|||
you can pass additional parameters to the filter: |
|||
|
|||
.. sourcecode:: jinja |
|||
|
|||
{{ mytext|indent(2, true) }} |
|||
indent by two spaces and indent the first line too. |
|||
""" |
|||
indention = u' ' * width |
|||
rv = (u'\n' + indention).join(s.splitlines()) |
|||
if indentfirst: |
|||
rv = indention + rv |
|||
return rv |
|||
|
|||
|
|||
def do_truncate(s, length=255, killwords=False, end='...'): |
|||
"""Return a truncated copy of the string. The length is specified |
|||
with the first parameter which defaults to ``255``. If the second |
|||
parameter is ``true`` the filter will cut the text at length. Otherwise |
|||
it will try to save the last word. If the text was in fact |
|||
truncated it will append an ellipsis sign (``"..."``). If you want a |
|||
different ellipsis sign than ``"..."`` you can specify it using the |
|||
third parameter. |
|||
|
|||
.. sourcecode jinja:: |
|||
|
|||
{{ mytext|truncate(300, false, '»') }} |
|||
truncate mytext to 300 chars, don't split up words, use a |
|||
right pointing double arrow as ellipsis sign. |
|||
""" |
|||
if len(s) <= length: |
|||
return s |
|||
elif killwords: |
|||
return s[:length] + end |
|||
words = s.split(' ') |
|||
result = [] |
|||
m = 0 |
|||
for word in words: |
|||
m += len(word) + 1 |
|||
if m > length: |
|||
break |
|||
result.append(word) |
|||
result.append(end) |
|||
return u' '.join(result) |
|||
|
|||
@environmentfilter |
|||
def do_wordwrap(environment, s, width=79, break_long_words=True): |
|||
""" |
|||
Return a copy of the string passed to the filter wrapped after |
|||
``79`` characters. You can override this default using the first |
|||
parameter. If you set the second parameter to `false` Jinja will not |
|||
split words apart if they are longer than `width`. |
|||
""" |
|||
import textwrap |
|||
return environment.newline_sequence.join(textwrap.wrap(s, width=width, expand_tabs=False, |
|||
replace_whitespace=False, |
|||
break_long_words=break_long_words)) |
|||
|
|||
|
|||
def do_wordcount(s): |
|||
"""Count the words in that string.""" |
|||
return len(_word_re.findall(s)) |
|||
|
|||
|
|||
def do_int(value, default=0): |
|||
"""Convert the value into an integer. If the |
|||
conversion doesn't work it will return ``0``. You can |
|||
override this default using the first parameter. |
|||
""" |
|||
try: |
|||
return int(value) |
|||
except (TypeError, ValueError): |
|||
# this quirk is necessary so that "42.23"|int gives 42. |
|||
try: |
|||
return int(float(value)) |
|||
except (TypeError, ValueError): |
|||
return default |
|||
|
|||
|
|||
def do_float(value, default=0.0): |
|||
"""Convert the value into a floating point number. If the |
|||
conversion doesn't work it will return ``0.0``. You can |
|||
override this default using the first parameter. |
|||
""" |
|||
try: |
|||
return float(value) |
|||
except (TypeError, ValueError): |
|||
return default |
|||
|
|||
|
|||
def do_format(value, *args, **kwargs): |
|||
""" |
|||
Apply python string formatting on an object: |
|||
|
|||
.. sourcecode:: jinja |
|||
|
|||
{{ "%s - %s"|format("Hello?", "Foo!") }} |
|||
-> Hello? - Foo! |
|||
""" |
|||
if args and kwargs: |
|||
raise FilterArgumentError('can\'t handle positional and keyword ' |
|||
'arguments at the same time') |
|||
return soft_unicode(value) % (kwargs or args) |
|||
|
|||
|
|||
def do_trim(value): |
|||
"""Strip leading and trailing whitespace.""" |
|||
return soft_unicode(value).strip() |
|||
|
|||
|
|||
def do_striptags(value): |
|||
"""Strip SGML/XML tags and replace adjacent whitespace by one space. |
|||
""" |
|||
if hasattr(value, '__html__'): |
|||
value = value.__html__() |
|||
return Markup(unicode(value)).striptags() |
|||
|
|||
|
|||
def do_slice(value, slices, fill_with=None): |
|||
"""Slice an iterator and return a list of lists containing |
|||
those items. Useful if you want to create a div containing |
|||
three ul tags that represent columns: |
|||
|
|||
.. sourcecode:: html+jinja |
|||
|
|||
<div class="columwrapper"> |
|||
{%- for column in items|slice(3) %} |
|||
<ul class="column-{{ loop.index }}"> |
|||
{%- for item in column %} |
|||
<li>{{ item }}</li> |
|||
{%- endfor %} |
|||
</ul> |
|||
{%- endfor %} |
|||
</div> |
|||
|
|||
If you pass it a second argument it's used to fill missing |
|||
values on the last iteration. |
|||
""" |
|||
seq = list(value) |
|||
length = len(seq) |
|||
items_per_slice = length // slices |
|||
slices_with_extra = length % slices |
|||
offset = 0 |
|||
for slice_number in xrange(slices): |
|||
start = offset + slice_number * items_per_slice |
|||
if slice_number < slices_with_extra: |
|||
offset += 1 |
|||
end = offset + (slice_number + 1) * items_per_slice |
|||
tmp = seq[start:end] |
|||
if fill_with is not None and slice_number >= slices_with_extra: |
|||
tmp.append(fill_with) |
|||
yield tmp |
|||
|
|||
|
|||
def do_batch(value, linecount, fill_with=None): |
|||
""" |
|||
A filter that batches items. It works pretty much like `slice` |
|||
just the other way round. It returns a list of lists with the |
|||
given number of items. If you provide a second parameter this |
|||
is used to fill missing items. See this example: |
|||
|
|||
.. sourcecode:: html+jinja |
|||
|
|||
<table> |
|||
{%- for row in items|batch(3, ' ') %} |
|||
<tr> |
|||
{%- for column in row %} |
|||
<td>{{ column }}</td> |
|||
{%- endfor %} |
|||
</tr> |
|||
{%- endfor %} |
|||
</table> |
|||
""" |
|||
result = [] |
|||
tmp = [] |
|||
for item in value: |
|||
if len(tmp) == linecount: |
|||
yield tmp |
|||
tmp = [] |
|||
tmp.append(item) |
|||
if tmp: |
|||
if fill_with is not None and len(tmp) < linecount: |
|||
tmp += [fill_with] * (linecount - len(tmp)) |
|||
yield tmp |
|||
|
|||
|
|||
def do_round(value, precision=0, method='common'): |
|||
"""Round the number to a given precision. The first |
|||
parameter specifies the precision (default is ``0``), the |
|||
second the rounding method: |
|||
|
|||
- ``'common'`` rounds either up or down |
|||
- ``'ceil'`` always rounds up |
|||
- ``'floor'`` always rounds down |
|||
|
|||
If you don't specify a method ``'common'`` is used. |
|||
|
|||
.. sourcecode:: jinja |
|||
|
|||
{{ 42.55|round }} |
|||
-> 43.0 |
|||
{{ 42.55|round(1, 'floor') }} |
|||
-> 42.5 |
|||
|
|||
Note that even if rounded to 0 precision, a float is returned. If |
|||
you need a real integer, pipe it through `int`: |
|||
|
|||
.. sourcecode:: jinja |
|||
|
|||
{{ 42.55|round|int }} |
|||
-> 43 |
|||
""" |
|||
if not method in ('common', 'ceil', 'floor'): |
|||
raise FilterArgumentError('method must be common, ceil or floor') |
|||
if method == 'common': |
|||
return round(value, precision) |
|||
func = getattr(math, method) |
|||
return func(value * (10 ** precision)) / (10 ** precision) |
|||
|
|||
|
|||
@environmentfilter |
|||
def do_groupby(environment, value, attribute): |
|||
"""Group a sequence of objects by a common attribute. |
|||
|
|||
If you for example have a list of dicts or objects that represent persons |
|||
with `gender`, `first_name` and `last_name` attributes and you want to |
|||
group all users by genders you can do something like the following |
|||
snippet: |
|||
|
|||
.. sourcecode:: html+jinja |
|||
|
|||
<ul> |
|||
{% for group in persons|groupby('gender') %} |
|||
<li>{{ group.grouper }}<ul> |
|||
{% for person in group.list %} |
|||
<li>{{ person.first_name }} {{ person.last_name }}</li> |
|||
{% endfor %}</ul></li> |
|||
{% endfor %} |
|||
</ul> |
|||
|
|||
Additionally it's possible to use tuple unpacking for the grouper and |
|||
list: |
|||
|
|||
.. sourcecode:: html+jinja |
|||
|
|||
<ul> |
|||
{% for grouper, list in persons|groupby('gender') %} |
|||
... |
|||
{% endfor %} |
|||
</ul> |
|||
|
|||
As you can see the item we're grouping by is stored in the `grouper` |
|||
attribute and the `list` contains all the objects that have this grouper |
|||
in common. |
|||
|
|||
.. versionchanged:: 2.6 |
|||
It's now possible to use dotted notation to group by the child |
|||
attribute of another attribute. |
|||
""" |
|||
expr = make_attrgetter(environment, attribute) |
|||
return sorted(map(_GroupTuple, groupby(sorted(value, key=expr), expr))) |
|||
|
|||
|
|||
class _GroupTuple(tuple): |
|||
__slots__ = () |
|||
grouper = property(itemgetter(0)) |
|||
list = property(itemgetter(1)) |
|||
|
|||
def __new__(cls, (key, value)): |
|||
return tuple.__new__(cls, (key, list(value))) |
|||
|
|||
|
|||
@environmentfilter |
|||
def do_sum(environment, iterable, attribute=None, start=0): |
|||
"""Returns the sum of a sequence of numbers plus the value of parameter |
|||
'start' (which defaults to 0). When the sequence is empty it returns |
|||
start. |
|||
|
|||
It is also possible to sum up only certain attributes: |
|||
|
|||
.. sourcecode:: jinja |
|||
|
|||
Total: {{ items|sum(attribute='price') }} |
|||
|
|||
.. versionchanged:: 2.6 |
|||
The `attribute` parameter was added to allow suming up over |
|||
attributes. Also the `start` parameter was moved on to the right. |
|||
""" |
|||
if attribute is not None: |
|||
iterable = imap(make_attrgetter(environment, attribute), iterable) |
|||
return sum(iterable, start) |
|||
|
|||
|
|||
def do_list(value): |
|||
"""Convert the value into a list. If it was a string the returned list |
|||
will be a list of characters. |
|||
""" |
|||
return list(value) |
|||
|
|||
|
|||
def do_mark_safe(value): |
|||
"""Mark the value as safe which means that in an environment with automatic |
|||
escaping enabled this variable will not be escaped. |
|||
""" |
|||
return Markup(value) |
|||
|
|||
|
|||
def do_mark_unsafe(value): |
|||
"""Mark a value as unsafe. This is the reverse operation for :func:`safe`.""" |
|||
return unicode(value) |
|||
|
|||
|
|||
def do_reverse(value): |
|||
"""Reverse the object or return an iterator the iterates over it the other |
|||
way round. |
|||
""" |
|||
if isinstance(value, basestring): |
|||
return value[::-1] |
|||
try: |
|||
return reversed(value) |
|||
except TypeError: |
|||
try: |
|||
rv = list(value) |
|||
rv.reverse() |
|||
return rv |
|||
except TypeError: |
|||
raise FilterArgumentError('argument must be iterable') |
|||
|
|||
|
|||
@environmentfilter |
|||
def do_attr(environment, obj, name): |
|||
"""Get an attribute of an object. ``foo|attr("bar")`` works like |
|||
``foo["bar"]`` just that always an attribute is returned and items are not |
|||
looked up. |
|||
|
|||
See :ref:`Notes on subscriptions <notes-on-subscriptions>` for more details. |
|||
""" |
|||
try: |
|||
name = str(name) |
|||
except UnicodeError: |
|||
pass |
|||
else: |
|||
try: |
|||
value = getattr(obj, name) |
|||
except AttributeError: |
|||
pass |
|||
else: |
|||
if environment.sandboxed and not \ |
|||
environment.is_safe_attribute(obj, name, value): |
|||
return environment.unsafe_undefined(obj, name) |
|||
return value |
|||
return environment.undefined(obj=obj, name=name) |
|||
|
|||
|
|||
FILTERS = { |
|||
'attr': do_attr, |
|||
'replace': do_replace, |
|||
'upper': do_upper, |
|||
'lower': do_lower, |
|||
'escape': escape, |
|||
'e': escape, |
|||
'forceescape': do_forceescape, |
|||
'capitalize': do_capitalize, |
|||
'title': do_title, |
|||
'default': do_default, |
|||
'd': do_default, |
|||
'join': do_join, |
|||
'count': len, |
|||
'dictsort': do_dictsort, |
|||
'sort': do_sort, |
|||
'length': len, |
|||
'reverse': do_reverse, |
|||
'center': do_center, |
|||
'indent': do_indent, |
|||
'title': do_title, |
|||
'capitalize': do_capitalize, |
|||
'first': do_first, |
|||
'last': do_last, |
|||
'random': do_random, |
|||
'filesizeformat': do_filesizeformat, |
|||
'pprint': do_pprint, |
|||
'truncate': do_truncate, |
|||
'wordwrap': do_wordwrap, |
|||
'wordcount': do_wordcount, |
|||
'int': do_int, |
|||
'float': do_float, |
|||
'string': soft_unicode, |
|||
'list': do_list, |
|||
'urlize': do_urlize, |
|||
'format': do_format, |
|||
'trim': do_trim, |
|||
'striptags': do_striptags, |
|||
'slice': do_slice, |
|||
'batch': do_batch, |
|||
'sum': do_sum, |
|||
'abs': abs, |
|||
'round': do_round, |
|||
'groupby': do_groupby, |
|||
'safe': do_mark_safe, |
|||
'xmlattr': do_xmlattr |
|||
} |
@ -1,681 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
jinja2.lexer |
|||
~~~~~~~~~~~~ |
|||
|
|||
This module implements a Jinja / Python combination lexer. The |
|||
`Lexer` class provided by this module is used to do some preprocessing |
|||
for Jinja. |
|||
|
|||
On the one hand it filters out invalid operators like the bitshift |
|||
operators we don't allow in templates. On the other hand it separates |
|||
template code and python code in expressions. |
|||
|
|||
:copyright: (c) 2010 by the Jinja Team. |
|||
:license: BSD, see LICENSE for more details. |
|||
""" |
|||
import re |
|||
from operator import itemgetter |
|||
from collections import deque |
|||
from jinja2.exceptions import TemplateSyntaxError |
|||
from jinja2.utils import LRUCache, next |
|||
|
|||
|
|||
# cache for the lexers. Exists in order to be able to have multiple |
|||
# environments with the same lexer |
|||
_lexer_cache = LRUCache(50) |
|||
|
|||
# static regular expressions |
|||
whitespace_re = re.compile(r'\s+', re.U) |
|||
string_re = re.compile(r"('([^'\\]*(?:\\.[^'\\]*)*)'" |
|||
r'|"([^"\\]*(?:\\.[^"\\]*)*)")', re.S) |
|||
integer_re = re.compile(r'\d+') |
|||
|
|||
# we use the unicode identifier rule if this python version is able |
|||
# to handle unicode identifiers, otherwise the standard ASCII one. |
|||
try: |
|||
compile('föö', '<unknown>', 'eval') |
|||
except SyntaxError: |
|||
name_re = re.compile(r'\b[a-zA-Z_][a-zA-Z0-9_]*\b') |
|||
else: |
|||
from jinja2 import _stringdefs |
|||
name_re = re.compile(r'[%s][%s]*' % (_stringdefs.xid_start, |
|||
_stringdefs.xid_continue)) |
|||
|
|||
float_re = re.compile(r'(?<!\.)\d+\.\d+') |
|||
newline_re = re.compile(r'(\r\n|\r|\n)') |
|||
|
|||
# internal the tokens and keep references to them |
|||
TOKEN_ADD = intern('add') |
|||
TOKEN_ASSIGN = intern('assign') |
|||
TOKEN_COLON = intern('colon') |
|||
TOKEN_COMMA = intern('comma') |
|||
TOKEN_DIV = intern('div') |
|||
TOKEN_DOT = intern('dot') |
|||
TOKEN_EQ = intern('eq') |
|||
TOKEN_FLOORDIV = intern('floordiv') |
|||
TOKEN_GT = intern('gt') |
|||
TOKEN_GTEQ = intern('gteq') |
|||
TOKEN_LBRACE = intern('lbrace') |
|||
TOKEN_LBRACKET = intern('lbracket') |
|||
TOKEN_LPAREN = intern('lparen') |
|||
TOKEN_LT = intern('lt') |
|||
TOKEN_LTEQ = intern('lteq') |
|||
TOKEN_MOD = intern('mod') |
|||
TOKEN_MUL = intern('mul') |
|||
TOKEN_NE = intern('ne') |
|||
TOKEN_PIPE = intern('pipe') |
|||
TOKEN_POW = intern('pow') |
|||
TOKEN_RBRACE = intern('rbrace') |
|||
TOKEN_RBRACKET = intern('rbracket') |
|||
TOKEN_RPAREN = intern('rparen') |
|||
TOKEN_SEMICOLON = intern('semicolon') |
|||
TOKEN_SUB = intern('sub') |
|||
TOKEN_TILDE = intern('tilde') |
|||
TOKEN_WHITESPACE = intern('whitespace') |
|||
TOKEN_FLOAT = intern('float') |
|||
TOKEN_INTEGER = intern('integer') |
|||
TOKEN_NAME = intern('name') |
|||
TOKEN_STRING = intern('string') |
|||
TOKEN_OPERATOR = intern('operator') |
|||
TOKEN_BLOCK_BEGIN = intern('block_begin') |
|||
TOKEN_BLOCK_END = intern('block_end') |
|||
TOKEN_VARIABLE_BEGIN = intern('variable_begin') |
|||
TOKEN_VARIABLE_END = intern('variable_end') |
|||
TOKEN_RAW_BEGIN = intern('raw_begin') |
|||
TOKEN_RAW_END = intern('raw_end') |
|||
TOKEN_COMMENT_BEGIN = intern('comment_begin') |
|||
TOKEN_COMMENT_END = intern('comment_end') |
|||
TOKEN_COMMENT = intern('comment') |
|||
TOKEN_LINESTATEMENT_BEGIN = intern('linestatement_begin') |
|||
TOKEN_LINESTATEMENT_END = intern('linestatement_end') |
|||
TOKEN_LINECOMMENT_BEGIN = intern('linecomment_begin') |
|||
TOKEN_LINECOMMENT_END = intern('linecomment_end') |
|||
TOKEN_LINECOMMENT = intern('linecomment') |
|||
TOKEN_DATA = intern('data') |
|||
TOKEN_INITIAL = intern('initial') |
|||
TOKEN_EOF = intern('eof') |
|||
|
|||
# bind operators to token types |
|||
operators = { |
|||
'+': TOKEN_ADD, |
|||
'-': TOKEN_SUB, |
|||
'/': TOKEN_DIV, |
|||
'//': TOKEN_FLOORDIV, |
|||
'*': TOKEN_MUL, |
|||
'%': TOKEN_MOD, |
|||
'**': TOKEN_POW, |
|||
'~': TOKEN_TILDE, |
|||
'[': TOKEN_LBRACKET, |
|||
']': TOKEN_RBRACKET, |
|||
'(': TOKEN_LPAREN, |
|||
')': TOKEN_RPAREN, |
|||
'{': TOKEN_LBRACE, |
|||
'}': TOKEN_RBRACE, |
|||
'==': TOKEN_EQ, |
|||
'!=': TOKEN_NE, |
|||
'>': TOKEN_GT, |
|||
'>=': TOKEN_GTEQ, |
|||
'<': TOKEN_LT, |
|||
'<=': TOKEN_LTEQ, |
|||
'=': TOKEN_ASSIGN, |
|||
'.': TOKEN_DOT, |
|||
':': TOKEN_COLON, |
|||
'|': TOKEN_PIPE, |
|||
',': TOKEN_COMMA, |
|||
';': TOKEN_SEMICOLON |
|||
} |
|||
|
|||
reverse_operators = dict([(v, k) for k, v in operators.iteritems()]) |
|||
assert len(operators) == len(reverse_operators), 'operators dropped' |
|||
operator_re = re.compile('(%s)' % '|'.join(re.escape(x) for x in |
|||
sorted(operators, key=lambda x: -len(x)))) |
|||
|
|||
ignored_tokens = frozenset([TOKEN_COMMENT_BEGIN, TOKEN_COMMENT, |
|||
TOKEN_COMMENT_END, TOKEN_WHITESPACE, |
|||
TOKEN_WHITESPACE, TOKEN_LINECOMMENT_BEGIN, |
|||
TOKEN_LINECOMMENT_END, TOKEN_LINECOMMENT]) |
|||
ignore_if_empty = frozenset([TOKEN_WHITESPACE, TOKEN_DATA, |
|||
TOKEN_COMMENT, TOKEN_LINECOMMENT]) |
|||
|
|||
|
|||
def _describe_token_type(token_type): |
|||
if token_type in reverse_operators: |
|||
return reverse_operators[token_type] |
|||
return { |
|||
TOKEN_COMMENT_BEGIN: 'begin of comment', |
|||
TOKEN_COMMENT_END: 'end of comment', |
|||
TOKEN_COMMENT: 'comment', |
|||
TOKEN_LINECOMMENT: 'comment', |
|||
TOKEN_BLOCK_BEGIN: 'begin of statement block', |
|||
TOKEN_BLOCK_END: 'end of statement block', |
|||
TOKEN_VARIABLE_BEGIN: 'begin of print statement', |
|||
TOKEN_VARIABLE_END: 'end of print statement', |
|||
TOKEN_LINESTATEMENT_BEGIN: 'begin of line statement', |
|||
TOKEN_LINESTATEMENT_END: 'end of line statement', |
|||
TOKEN_DATA: 'template data / text', |
|||
TOKEN_EOF: 'end of template' |
|||
}.get(token_type, token_type) |
|||
|
|||
|
|||
def describe_token(token): |
|||
"""Returns a description of the token.""" |
|||
if token.type == 'name': |
|||
return token.value |
|||
return _describe_token_type(token.type) |
|||
|
|||
|
|||
def describe_token_expr(expr): |
|||
"""Like `describe_token` but for token expressions.""" |
|||
if ':' in expr: |
|||
type, value = expr.split(':', 1) |
|||
if type == 'name': |
|||
return value |
|||
else: |
|||
type = expr |
|||
return _describe_token_type(type) |
|||
|
|||
|
|||
def count_newlines(value): |
|||
"""Count the number of newline characters in the string. This is |
|||
useful for extensions that filter a stream. |
|||
""" |
|||
return len(newline_re.findall(value)) |
|||
|
|||
|
|||
def compile_rules(environment): |
|||
"""Compiles all the rules from the environment into a list of rules.""" |
|||
e = re.escape |
|||
rules = [ |
|||
(len(environment.comment_start_string), 'comment', |
|||
e(environment.comment_start_string)), |
|||
(len(environment.block_start_string), 'block', |
|||
e(environment.block_start_string)), |
|||
(len(environment.variable_start_string), 'variable', |
|||
e(environment.variable_start_string)) |
|||
] |
|||
|
|||
if environment.line_statement_prefix is not None: |
|||
rules.append((len(environment.line_statement_prefix), 'linestatement', |
|||
r'^\s*' + e(environment.line_statement_prefix))) |
|||
if environment.line_comment_prefix is not None: |
|||
rules.append((len(environment.line_comment_prefix), 'linecomment', |
|||
r'(?:^|(?<=\S))[^\S\r\n]*' + |
|||
e(environment.line_comment_prefix))) |
|||
|
|||
return [x[1:] for x in sorted(rules, reverse=True)] |
|||
|
|||
|
|||
class Failure(object): |
|||
"""Class that raises a `TemplateSyntaxError` if called. |
|||
Used by the `Lexer` to specify known errors. |
|||
""" |
|||
|
|||
def __init__(self, message, cls=TemplateSyntaxError): |
|||
self.message = message |
|||
self.error_class = cls |
|||
|
|||
def __call__(self, lineno, filename): |
|||
raise self.error_class(self.message, lineno, filename) |
|||
|
|||
|
|||
class Token(tuple): |
|||
"""Token class.""" |
|||
__slots__ = () |
|||
lineno, type, value = (property(itemgetter(x)) for x in range(3)) |
|||
|
|||
def __new__(cls, lineno, type, value): |
|||
return tuple.__new__(cls, (lineno, intern(str(type)), value)) |
|||
|
|||
def __str__(self): |
|||
if self.type in reverse_operators: |
|||
return reverse_operators[self.type] |
|||
elif self.type == 'name': |
|||
return self.value |
|||
return self.type |
|||
|
|||
def test(self, expr): |
|||
"""Test a token against a token expression. This can either be a |
|||
token type or ``'token_type:token_value'``. This can only test |
|||
against string values and types. |
|||
""" |
|||
# here we do a regular string equality check as test_any is usually |
|||
# passed an iterable of not interned strings. |
|||
if self.type == expr: |
|||
return True |
|||
elif ':' in expr: |
|||
return expr.split(':', 1) == [self.type, self.value] |
|||
return False |
|||
|
|||
def test_any(self, *iterable): |
|||
"""Test against multiple token expressions.""" |
|||
for expr in iterable: |
|||
if self.test(expr): |
|||
return True |
|||
return False |
|||
|
|||
def __repr__(self): |
|||
return 'Token(%r, %r, %r)' % ( |
|||
self.lineno, |
|||
self.type, |
|||
self.value |
|||
) |
|||
|
|||
|
|||
class TokenStreamIterator(object): |
|||
"""The iterator for tokenstreams. Iterate over the stream |
|||
until the eof token is reached. |
|||
""" |
|||
|
|||
def __init__(self, stream): |
|||
self.stream = stream |
|||
|
|||
def __iter__(self): |
|||
return self |
|||
|
|||
def next(self): |
|||
token = self.stream.current |
|||
if token.type is TOKEN_EOF: |
|||
self.stream.close() |
|||
raise StopIteration() |
|||
next(self.stream) |
|||
return token |
|||
|
|||
|
|||
class TokenStream(object): |
|||
"""A token stream is an iterable that yields :class:`Token`\s. The |
|||
parser however does not iterate over it but calls :meth:`next` to go |
|||
one token ahead. The current active token is stored as :attr:`current`. |
|||
""" |
|||
|
|||
def __init__(self, generator, name, filename): |
|||
self._next = iter(generator).next |
|||
self._pushed = deque() |
|||
self.name = name |
|||
self.filename = filename |
|||
self.closed = False |
|||
self.current = Token(1, TOKEN_INITIAL, '') |
|||
next(self) |
|||
|
|||
def __iter__(self): |
|||
return TokenStreamIterator(self) |
|||
|
|||
def __nonzero__(self): |
|||
return bool(self._pushed) or self.current.type is not TOKEN_EOF |
|||
|
|||
eos = property(lambda x: not x, doc="Are we at the end of the stream?") |
|||
|
|||
def push(self, token): |
|||
"""Push a token back to the stream.""" |
|||
self._pushed.append(token) |
|||
|
|||
def look(self): |
|||
"""Look at the next token.""" |
|||
old_token = next(self) |
|||
result = self.current |
|||
self.push(result) |
|||
self.current = old_token |
|||
return result |
|||
|
|||
def skip(self, n=1): |
|||
"""Got n tokens ahead.""" |
|||
for x in xrange(n): |
|||
next(self) |
|||
|
|||
def next_if(self, expr): |
|||
"""Perform the token test and return the token if it matched. |
|||
Otherwise the return value is `None`. |
|||
""" |
|||
if self.current.test(expr): |
|||
return next(self) |
|||
|
|||
def skip_if(self, expr): |
|||
"""Like :meth:`next_if` but only returns `True` or `False`.""" |
|||
return self.next_if(expr) is not None |
|||
|
|||
def next(self): |
|||
"""Go one token ahead and return the old one""" |
|||
rv = self.current |
|||
if self._pushed: |
|||
self.current = self._pushed.popleft() |
|||
elif self.current.type is not TOKEN_EOF: |
|||
try: |
|||
self.current = self._next() |
|||
except StopIteration: |
|||
self.close() |
|||
return rv |
|||
|
|||
def close(self): |
|||
"""Close the stream.""" |
|||
self.current = Token(self.current.lineno, TOKEN_EOF, '') |
|||
self._next = None |
|||
self.closed = True |
|||
|
|||
def expect(self, expr): |
|||
"""Expect a given token type and return it. This accepts the same |
|||
argument as :meth:`jinja2.lexer.Token.test`. |
|||
""" |
|||
if not self.current.test(expr): |
|||
expr = describe_token_expr(expr) |
|||
if self.current.type is TOKEN_EOF: |
|||
raise TemplateSyntaxError('unexpected end of template, ' |
|||
'expected %r.' % expr, |
|||
self.current.lineno, |
|||
self.name, self.filename) |
|||
raise TemplateSyntaxError("expected token %r, got %r" % |
|||
(expr, describe_token(self.current)), |
|||
self.current.lineno, |
|||
self.name, self.filename) |
|||
try: |
|||
return self.current |
|||
finally: |
|||
next(self) |
|||
|
|||
|
|||
def get_lexer(environment): |
|||
"""Return a lexer which is probably cached.""" |
|||
key = (environment.block_start_string, |
|||
environment.block_end_string, |
|||
environment.variable_start_string, |
|||
environment.variable_end_string, |
|||
environment.comment_start_string, |
|||
environment.comment_end_string, |
|||
environment.line_statement_prefix, |
|||
environment.line_comment_prefix, |
|||
environment.trim_blocks, |
|||
environment.newline_sequence) |
|||
lexer = _lexer_cache.get(key) |
|||
if lexer is None: |
|||
lexer = Lexer(environment) |
|||
_lexer_cache[key] = lexer |
|||
return lexer |
|||
|
|||
|
|||
class Lexer(object): |
|||
"""Class that implements a lexer for a given environment. Automatically |
|||
created by the environment class, usually you don't have to do that. |
|||
|
|||
Note that the lexer is not automatically bound to an environment. |
|||
Multiple environments can share the same lexer. |
|||
""" |
|||
|
|||
def __init__(self, environment): |
|||
# shortcuts |
|||
c = lambda x: re.compile(x, re.M | re.S) |
|||
e = re.escape |
|||
|
|||
# lexing rules for tags |
|||
tag_rules = [ |
|||
(whitespace_re, TOKEN_WHITESPACE, None), |
|||
(float_re, TOKEN_FLOAT, None), |
|||
(integer_re, TOKEN_INTEGER, None), |
|||
(name_re, TOKEN_NAME, None), |
|||
(string_re, TOKEN_STRING, None), |
|||
(operator_re, TOKEN_OPERATOR, None) |
|||
] |
|||
|
|||
# assamble the root lexing rule. because "|" is ungreedy |
|||
# we have to sort by length so that the lexer continues working |
|||
# as expected when we have parsing rules like <% for block and |
|||
# <%= for variables. (if someone wants asp like syntax) |
|||
# variables are just part of the rules if variable processing |
|||
# is required. |
|||
root_tag_rules = compile_rules(environment) |
|||
|
|||
# block suffix if trimming is enabled |
|||
block_suffix_re = environment.trim_blocks and '\\n?' or '' |
|||
|
|||
self.newline_sequence = environment.newline_sequence |
|||
|
|||
# global lexing rules |
|||
self.rules = { |
|||
'root': [ |
|||
# directives |
|||
(c('(.*?)(?:%s)' % '|'.join( |
|||
[r'(?P<raw_begin>(?:\s*%s\-|%s)\s*raw\s*(?:\-%s\s*|%s))' % ( |
|||
e(environment.block_start_string), |
|||
e(environment.block_start_string), |
|||
e(environment.block_end_string), |
|||
e(environment.block_end_string) |
|||
)] + [ |
|||
r'(?P<%s_begin>\s*%s\-|%s)' % (n, r, r) |
|||
for n, r in root_tag_rules |
|||
])), (TOKEN_DATA, '#bygroup'), '#bygroup'), |
|||
# data |
|||
(c('.+'), TOKEN_DATA, None) |
|||
], |
|||
# comments |
|||
TOKEN_COMMENT_BEGIN: [ |
|||
(c(r'(.*?)((?:\-%s\s*|%s)%s)' % ( |
|||
e(environment.comment_end_string), |
|||
e(environment.comment_end_string), |
|||
block_suffix_re |
|||
)), (TOKEN_COMMENT, TOKEN_COMMENT_END), '#pop'), |
|||
(c('(.)'), (Failure('Missing end of comment tag'),), None) |
|||
], |
|||
# blocks |
|||
TOKEN_BLOCK_BEGIN: [ |
|||
(c('(?:\-%s\s*|%s)%s' % ( |
|||
e(environment.block_end_string), |
|||
e(environment.block_end_string), |
|||
block_suffix_re |
|||
)), TOKEN_BLOCK_END, '#pop'), |
|||
] + tag_rules, |
|||
# variables |
|||
TOKEN_VARIABLE_BEGIN: [ |
|||
(c('\-%s\s*|%s' % ( |
|||
e(environment.variable_end_string), |
|||
e(environment.variable_end_string) |
|||
)), TOKEN_VARIABLE_END, '#pop') |
|||
] + tag_rules, |
|||
# raw block |
|||
TOKEN_RAW_BEGIN: [ |
|||
(c('(.*?)((?:\s*%s\-|%s)\s*endraw\s*(?:\-%s\s*|%s%s))' % ( |
|||
e(environment.block_start_string), |
|||
e(environment.block_start_string), |
|||
e(environment.block_end_string), |
|||
e(environment.block_end_string), |
|||
block_suffix_re |
|||
)), (TOKEN_DATA, TOKEN_RAW_END), '#pop'), |
|||
(c('(.)'), (Failure('Missing end of raw directive'),), None) |
|||
], |
|||
# line statements |
|||
TOKEN_LINESTATEMENT_BEGIN: [ |
|||
(c(r'\s*(\n|$)'), TOKEN_LINESTATEMENT_END, '#pop') |
|||
] + tag_rules, |
|||
# line comments |
|||
TOKEN_LINECOMMENT_BEGIN: [ |
|||
(c(r'(.*?)()(?=\n|$)'), (TOKEN_LINECOMMENT, |
|||
TOKEN_LINECOMMENT_END), '#pop') |
|||
] |
|||
} |
|||
|
|||
def _normalize_newlines(self, value): |
|||
"""Called for strings and template data to normlize it to unicode.""" |
|||
return newline_re.sub(self.newline_sequence, value) |
|||
|
|||
def tokenize(self, source, name=None, filename=None, state=None): |
|||
"""Calls tokeniter + tokenize and wraps it in a token stream. |
|||
""" |
|||
stream = self.tokeniter(source, name, filename, state) |
|||
return TokenStream(self.wrap(stream, name, filename), name, filename) |
|||
|
|||
def wrap(self, stream, name=None, filename=None): |
|||
"""This is called with the stream as returned by `tokenize` and wraps |
|||
every token in a :class:`Token` and converts the value. |
|||
""" |
|||
for lineno, token, value in stream: |
|||
if token in ignored_tokens: |
|||
continue |
|||
elif token == 'linestatement_begin': |
|||
token = 'block_begin' |
|||
elif token == 'linestatement_end': |
|||
token = 'block_end' |
|||
# we are not interested in those tokens in the parser |
|||
elif token in ('raw_begin', 'raw_end'): |
|||
continue |
|||
elif token == 'data': |
|||
value = self._normalize_newlines(value) |
|||
elif token == 'keyword': |
|||
token = value |
|||
elif token == 'name': |
|||
value = str(value) |
|||
elif token == 'string': |
|||
# try to unescape string |
|||
try: |
|||
value = self._normalize_newlines(value[1:-1]) \ |
|||
.encode('ascii', 'backslashreplace') \ |
|||
.decode('unicode-escape') |
|||
except Exception, e: |
|||
msg = str(e).split(':')[-1].strip() |
|||
raise TemplateSyntaxError(msg, lineno, name, filename) |
|||
# if we can express it as bytestring (ascii only) |
|||
# we do that for support of semi broken APIs |
|||
# as datetime.datetime.strftime. On python 3 this |
|||
# call becomes a noop thanks to 2to3 |
|||
try: |
|||
value = str(value) |
|||
except UnicodeError: |
|||
pass |
|||
elif token == 'integer': |
|||
value = int(value) |
|||
elif token == 'float': |
|||
value = float(value) |
|||
elif token == 'operator': |
|||
token = operators[value] |
|||
yield Token(lineno, token, value) |
|||
|
|||
def tokeniter(self, source, name, filename=None, state=None): |
|||
"""This method tokenizes the text and returns the tokens in a |
|||
generator. Use this method if you just want to tokenize a template. |
|||
""" |
|||
source = '\n'.join(unicode(source).splitlines()) |
|||
pos = 0 |
|||
lineno = 1 |
|||
stack = ['root'] |
|||
if state is not None and state != 'root': |
|||
assert state in ('variable', 'block'), 'invalid state' |
|||
stack.append(state + '_begin') |
|||
else: |
|||
state = 'root' |
|||
statetokens = self.rules[stack[-1]] |
|||
source_length = len(source) |
|||
|
|||
balancing_stack = [] |
|||
|
|||
while 1: |
|||
# tokenizer loop |
|||
for regex, tokens, new_state in statetokens: |
|||
m = regex.match(source, pos) |
|||
# if no match we try again with the next rule |
|||
if m is None: |
|||
continue |
|||
|
|||
# we only match blocks and variables if brances / parentheses |
|||
# are balanced. continue parsing with the lower rule which |
|||
# is the operator rule. do this only if the end tags look |
|||
# like operators |
|||
if balancing_stack and \ |
|||
tokens in ('variable_end', 'block_end', |
|||
'linestatement_end'): |
|||
continue |
|||
|
|||
# tuples support more options |
|||
if isinstance(tokens, tuple): |
|||
for idx, token in enumerate(tokens): |
|||
# failure group |
|||
if token.__class__ is Failure: |
|||
raise token(lineno, filename) |
|||
# bygroup is a bit more complex, in that case we |
|||
# yield for the current token the first named |
|||
# group that matched |
|||
elif token == '#bygroup': |
|||
for key, value in m.groupdict().iteritems(): |
|||
if value is not None: |
|||
yield lineno, key, value |
|||
lineno += value.count('\n') |
|||
break |
|||
else: |
|||
raise RuntimeError('%r wanted to resolve ' |
|||
'the token dynamically' |
|||
' but no group matched' |
|||
% regex) |
|||
# normal group |
|||
else: |
|||
data = m.group(idx + 1) |
|||
if data or token not in ignore_if_empty: |
|||
yield lineno, token, data |
|||
lineno += data.count('\n') |
|||
|
|||
# strings as token just are yielded as it. |
|||
else: |
|||
data = m.group() |
|||
# update brace/parentheses balance |
|||
if tokens == 'operator': |
|||
if data == '{': |
|||
balancing_stack.append('}') |
|||
elif data == '(': |
|||
balancing_stack.append(')') |
|||
elif data == '[': |
|||
balancing_stack.append(']') |
|||
elif data in ('}', ')', ']'): |
|||
if not balancing_stack: |
|||
raise TemplateSyntaxError('unexpected \'%s\'' % |
|||
data, lineno, name, |
|||
filename) |
|||
expected_op = balancing_stack.pop() |
|||
if expected_op != data: |
|||
raise TemplateSyntaxError('unexpected \'%s\', ' |
|||
'expected \'%s\'' % |
|||
(data, expected_op), |
|||
lineno, name, |
|||
filename) |
|||
# yield items |
|||
if data or tokens not in ignore_if_empty: |
|||
yield lineno, tokens, data |
|||
lineno += data.count('\n') |
|||
|
|||
# fetch new position into new variable so that we can check |
|||
# if there is a internal parsing error which would result |
|||
# in an infinite loop |
|||
pos2 = m.end() |
|||
|
|||
# handle state changes |
|||
if new_state is not None: |
|||
# remove the uppermost state |
|||
if new_state == '#pop': |
|||
stack.pop() |
|||
# resolve the new state by group checking |
|||
elif new_state == '#bygroup': |
|||
for key, value in m.groupdict().iteritems(): |
|||
if value is not None: |
|||
stack.append(key) |
|||
break |
|||
else: |
|||
raise RuntimeError('%r wanted to resolve the ' |
|||
'new state dynamically but' |
|||
' no group matched' % |
|||
regex) |
|||
# direct state name given |
|||
else: |
|||
stack.append(new_state) |
|||
statetokens = self.rules[stack[-1]] |
|||
# we are still at the same position and no stack change. |
|||
# this means a loop without break condition, avoid that and |
|||
# raise error |
|||
elif pos2 == pos: |
|||
raise RuntimeError('%r yielded empty string without ' |
|||
'stack change' % regex) |
|||
# publish new function and start again |
|||
pos = pos2 |
|||
break |
|||
# if loop terminated without break we havn't found a single match |
|||
# either we are at the end of the file or we have a problem |
|||
else: |
|||
# end of text |
|||
if pos >= source_length: |
|||
return |
|||
# something went wrong |
|||
raise TemplateSyntaxError('unexpected char %r at %d' % |
|||
(source[pos], pos), lineno, |
|||
name, filename) |
@ -1,450 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
jinja2.loaders |
|||
~~~~~~~~~~~~~~ |
|||
|
|||
Jinja loader classes. |
|||
|
|||
:copyright: (c) 2010 by the Jinja Team. |
|||
:license: BSD, see LICENSE for more details. |
|||
""" |
|||
import os |
|||
import sys |
|||
import weakref |
|||
from types import ModuleType |
|||
from os import path |
|||
try: |
|||
from hashlib import sha1 |
|||
except ImportError: |
|||
from sha import new as sha1 |
|||
from jinja2.exceptions import TemplateNotFound |
|||
from jinja2.utils import LRUCache, open_if_exists, internalcode |
|||
|
|||
|
|||
def split_template_path(template): |
|||
"""Split a path into segments and perform a sanity check. If it detects |
|||
'..' in the path it will raise a `TemplateNotFound` error. |
|||
""" |
|||
pieces = [] |
|||
for piece in template.split('/'): |
|||
if path.sep in piece \ |
|||
or (path.altsep and path.altsep in piece) or \ |
|||
piece == path.pardir: |
|||
raise TemplateNotFound(template) |
|||
elif piece and piece != '.': |
|||
pieces.append(piece) |
|||
return pieces |
|||
|
|||
|
|||
class BaseLoader(object): |
|||
"""Baseclass for all loaders. Subclass this and override `get_source` to |
|||
implement a custom loading mechanism. The environment provides a |
|||
`get_template` method that calls the loader's `load` method to get the |
|||
:class:`Template` object. |
|||
|
|||
A very basic example for a loader that looks up templates on the file |
|||
system could look like this:: |
|||
|
|||
from jinja2 import BaseLoader, TemplateNotFound |
|||
from os.path import join, exists, getmtime |
|||
|
|||
class MyLoader(BaseLoader): |
|||
|
|||
def __init__(self, path): |
|||
self.path = path |
|||
|
|||
def get_source(self, environment, template): |
|||
path = join(self.path, template) |
|||
if not exists(path): |
|||
raise TemplateNotFound(template) |
|||
mtime = getmtime(path) |
|||
with file(path) as f: |
|||
source = f.read().decode('utf-8') |
|||
return source, path, lambda: mtime == getmtime(path) |
|||
""" |
|||
|
|||
#: if set to `False` it indicates that the loader cannot provide access |
|||
#: to the source of templates. |
|||
#: |
|||
#: .. versionadded:: 2.4 |
|||
has_source_access = True |
|||
|
|||
def get_source(self, environment, template): |
|||
"""Get the template source, filename and reload helper for a template. |
|||
It's passed the environment and template name and has to return a |
|||
tuple in the form ``(source, filename, uptodate)`` or raise a |
|||
`TemplateNotFound` error if it can't locate the template. |
|||
|
|||
The source part of the returned tuple must be the source of the |
|||
template as unicode string or a ASCII bytestring. The filename should |
|||
be the name of the file on the filesystem if it was loaded from there, |
|||
otherwise `None`. The filename is used by python for the tracebacks |
|||
if no loader extension is used. |
|||
|
|||
The last item in the tuple is the `uptodate` function. If auto |
|||
reloading is enabled it's always called to check if the template |
|||
changed. No arguments are passed so the function must store the |
|||
old state somewhere (for example in a closure). If it returns `False` |
|||
the template will be reloaded. |
|||
""" |
|||
if not self.has_source_access: |
|||
raise RuntimeError('%s cannot provide access to the source' % |
|||
self.__class__.__name__) |
|||
raise TemplateNotFound(template) |
|||
|
|||
def list_templates(self): |
|||
"""Iterates over all templates. If the loader does not support that |
|||
it should raise a :exc:`TypeError` which is the default behavior. |
|||
""" |
|||
raise TypeError('this loader cannot iterate over all templates') |
|||
|
|||
@internalcode |
|||
def load(self, environment, name, globals=None): |
|||
"""Loads a template. This method looks up the template in the cache |
|||
or loads one by calling :meth:`get_source`. Subclasses should not |
|||
override this method as loaders working on collections of other |
|||
loaders (such as :class:`PrefixLoader` or :class:`ChoiceLoader`) |
|||
will not call this method but `get_source` directly. |
|||
""" |
|||
code = None |
|||
if globals is None: |
|||
globals = {} |
|||
|
|||
# first we try to get the source for this template together |
|||
# with the filename and the uptodate function. |
|||
source, filename, uptodate = self.get_source(environment, name) |
|||
|
|||
# try to load the code from the bytecode cache if there is a |
|||
# bytecode cache configured. |
|||
bcc = environment.bytecode_cache |
|||
if bcc is not None: |
|||
bucket = bcc.get_bucket(environment, name, filename, source) |
|||
code = bucket.code |
|||
|
|||
# if we don't have code so far (not cached, no longer up to |
|||
# date) etc. we compile the template |
|||
if code is None: |
|||
code = environment.compile(source, name, filename) |
|||
|
|||
# if the bytecode cache is available and the bucket doesn't |
|||
# have a code so far, we give the bucket the new code and put |
|||
# it back to the bytecode cache. |
|||
if bcc is not None and bucket.code is None: |
|||
bucket.code = code |
|||
bcc.set_bucket(bucket) |
|||
|
|||
return environment.template_class.from_code(environment, code, |
|||
globals, uptodate) |
|||
|
|||
|
|||
class FileSystemLoader(BaseLoader): |
|||
"""Loads templates from the file system. This loader can find templates |
|||
in folders on the file system and is the preferred way to load them. |
|||
|
|||
The loader takes the path to the templates as string, or if multiple |
|||
locations are wanted a list of them which is then looked up in the |
|||
given order: |
|||
|
|||
>>> loader = FileSystemLoader('/path/to/templates') |
|||
>>> loader = FileSystemLoader(['/path/to/templates', '/other/path']) |
|||
|
|||
Per default the template encoding is ``'utf-8'`` which can be changed |
|||
by setting the `encoding` parameter to something else. |
|||
""" |
|||
|
|||
def __init__(self, searchpath, encoding='utf-8'): |
|||
if isinstance(searchpath, basestring): |
|||
searchpath = [searchpath] |
|||
self.searchpath = list(searchpath) |
|||
self.encoding = encoding |
|||
|
|||
def get_source(self, environment, template): |
|||
pieces = split_template_path(template) |
|||
for searchpath in self.searchpath: |
|||
filename = path.join(searchpath, *pieces) |
|||
f = open_if_exists(filename) |
|||
if f is None: |
|||
continue |
|||
try: |
|||
contents = f.read().decode(self.encoding) |
|||
finally: |
|||
f.close() |
|||
|
|||
mtime = path.getmtime(filename) |
|||
def uptodate(): |
|||
try: |
|||
return path.getmtime(filename) == mtime |
|||
except OSError: |
|||
return False |
|||
return contents, filename, uptodate |
|||
raise TemplateNotFound(template) |
|||
|
|||
def list_templates(self): |
|||
found = set() |
|||
for searchpath in self.searchpath: |
|||
for dirpath, dirnames, filenames in os.walk(searchpath): |
|||
for filename in filenames: |
|||
template = os.path.join(dirpath, filename) \ |
|||
[len(searchpath):].strip(os.path.sep) \ |
|||
.replace(os.path.sep, '/') |
|||
if template[:2] == './': |
|||
template = template[2:] |
|||
if template not in found: |
|||
found.add(template) |
|||
return sorted(found) |
|||
|
|||
|
|||
class PackageLoader(BaseLoader): |
|||
"""Load templates from python eggs or packages. It is constructed with |
|||
the name of the python package and the path to the templates in that |
|||
package:: |
|||
|
|||
loader = PackageLoader('mypackage', 'views') |
|||
|
|||
If the package path is not given, ``'templates'`` is assumed. |
|||
|
|||
Per default the template encoding is ``'utf-8'`` which can be changed |
|||
by setting the `encoding` parameter to something else. Due to the nature |
|||
of eggs it's only possible to reload templates if the package was loaded |
|||
from the file system and not a zip file. |
|||
""" |
|||
|
|||
def __init__(self, package_name, package_path='templates', |
|||
encoding='utf-8'): |
|||
from pkg_resources import DefaultProvider, ResourceManager, \ |
|||
get_provider |
|||
provider = get_provider(package_name) |
|||
self.encoding = encoding |
|||
self.manager = ResourceManager() |
|||
self.filesystem_bound = isinstance(provider, DefaultProvider) |
|||
self.provider = provider |
|||
self.package_path = package_path |
|||
|
|||
def get_source(self, environment, template): |
|||
pieces = split_template_path(template) |
|||
p = '/'.join((self.package_path,) + tuple(pieces)) |
|||
if not self.provider.has_resource(p): |
|||
raise TemplateNotFound(template) |
|||
|
|||
filename = uptodate = None |
|||
if self.filesystem_bound: |
|||
filename = self.provider.get_resource_filename(self.manager, p) |
|||
mtime = path.getmtime(filename) |
|||
def uptodate(): |
|||
try: |
|||
return path.getmtime(filename) == mtime |
|||
except OSError: |
|||
return False |
|||
|
|||
source = self.provider.get_resource_string(self.manager, p) |
|||
return source.decode(self.encoding), filename, uptodate |
|||
|
|||
def list_templates(self): |
|||
path = self.package_path |
|||
if path[:2] == './': |
|||
path = path[2:] |
|||
elif path == '.': |
|||
path = '' |
|||
offset = len(path) |
|||
results = [] |
|||
def _walk(path): |
|||
for filename in self.provider.resource_listdir(path): |
|||
fullname = path + '/' + filename |
|||
if self.provider.resource_isdir(fullname): |
|||
_walk(fullname) |
|||
else: |
|||
results.append(fullname[offset:].lstrip('/')) |
|||
_walk(path) |
|||
results.sort() |
|||
return results |
|||
|
|||
|
|||
class DictLoader(BaseLoader): |
|||
"""Loads a template from a python dict. It's passed a dict of unicode |
|||
strings bound to template names. This loader is useful for unittesting: |
|||
|
|||
>>> loader = DictLoader({'index.html': 'source here'}) |
|||
|
|||
Because auto reloading is rarely useful this is disabled per default. |
|||
""" |
|||
|
|||
def __init__(self, mapping): |
|||
self.mapping = mapping |
|||
|
|||
def get_source(self, environment, template): |
|||
if template in self.mapping: |
|||
source = self.mapping[template] |
|||
return source, None, lambda: source != self.mapping.get(template) |
|||
raise TemplateNotFound(template) |
|||
|
|||
def list_templates(self): |
|||
return sorted(self.mapping) |
|||
|
|||
|
|||
class FunctionLoader(BaseLoader): |
|||
"""A loader that is passed a function which does the loading. The |
|||
function becomes the name of the template passed and has to return either |
|||
an unicode string with the template source, a tuple in the form ``(source, |
|||
filename, uptodatefunc)`` or `None` if the template does not exist. |
|||
|
|||
>>> def load_template(name): |
|||
... if name == 'index.html': |
|||
... return '...' |
|||
... |
|||
>>> loader = FunctionLoader(load_template) |
|||
|
|||
The `uptodatefunc` is a function that is called if autoreload is enabled |
|||
and has to return `True` if the template is still up to date. For more |
|||
details have a look at :meth:`BaseLoader.get_source` which has the same |
|||
return value. |
|||
""" |
|||
|
|||
def __init__(self, load_func): |
|||
self.load_func = load_func |
|||
|
|||
def get_source(self, environment, template): |
|||
rv = self.load_func(template) |
|||
if rv is None: |
|||
raise TemplateNotFound(template) |
|||
elif isinstance(rv, basestring): |
|||
return rv, None, None |
|||
return rv |
|||
|
|||
|
|||
class PrefixLoader(BaseLoader): |
|||
"""A loader that is passed a dict of loaders where each loader is bound |
|||
to a prefix. The prefix is delimited from the template by a slash per |
|||
default, which can be changed by setting the `delimiter` argument to |
|||
something else:: |
|||
|
|||
loader = PrefixLoader({ |
|||
'app1': PackageLoader('mypackage.app1'), |
|||
'app2': PackageLoader('mypackage.app2') |
|||
}) |
|||
|
|||
By loading ``'app1/index.html'`` the file from the app1 package is loaded, |
|||
by loading ``'app2/index.html'`` the file from the second. |
|||
""" |
|||
|
|||
def __init__(self, mapping, delimiter='/'): |
|||
self.mapping = mapping |
|||
self.delimiter = delimiter |
|||
|
|||
def get_source(self, environment, template): |
|||
try: |
|||
prefix, name = template.split(self.delimiter, 1) |
|||
loader = self.mapping[prefix] |
|||
except (ValueError, KeyError): |
|||
raise TemplateNotFound(template) |
|||
try: |
|||
return loader.get_source(environment, name) |
|||
except TemplateNotFound: |
|||
# re-raise the exception with the correct fileame here. |
|||
# (the one that includes the prefix) |
|||
raise TemplateNotFound(template) |
|||
|
|||
def list_templates(self): |
|||
result = [] |
|||
for prefix, loader in self.mapping.iteritems(): |
|||
for template in loader.list_templates(): |
|||
result.append(prefix + self.delimiter + template) |
|||
return result |
|||
|
|||
|
|||
class ChoiceLoader(BaseLoader): |
|||
"""This loader works like the `PrefixLoader` just that no prefix is |
|||
specified. If a template could not be found by one loader the next one |
|||
is tried. |
|||
|
|||
>>> loader = ChoiceLoader([ |
|||
... FileSystemLoader('/path/to/user/templates'), |
|||
... FileSystemLoader('/path/to/system/templates') |
|||
... ]) |
|||
|
|||
This is useful if you want to allow users to override builtin templates |
|||
from a different location. |
|||
""" |
|||
|
|||
def __init__(self, loaders): |
|||
self.loaders = loaders |
|||
|
|||
def get_source(self, environment, template): |
|||
for loader in self.loaders: |
|||
try: |
|||
return loader.get_source(environment, template) |
|||
except TemplateNotFound: |
|||
pass |
|||
raise TemplateNotFound(template) |
|||
|
|||
def list_templates(self): |
|||
found = set() |
|||
for loader in self.loaders: |
|||
found.update(loader.list_templates()) |
|||
return sorted(found) |
|||
|
|||
|
|||
class _TemplateModule(ModuleType): |
|||
"""Like a normal module but with support for weak references""" |
|||
|
|||
|
|||
class ModuleLoader(BaseLoader): |
|||
"""This loader loads templates from precompiled templates. |
|||
|
|||
Example usage: |
|||
|
|||
>>> loader = ChoiceLoader([ |
|||
... ModuleLoader('/path/to/compiled/templates'), |
|||
... FileSystemLoader('/path/to/templates') |
|||
... ]) |
|||
|
|||
Templates can be precompiled with :meth:`Environment.compile_templates`. |
|||
""" |
|||
|
|||
has_source_access = False |
|||
|
|||
def __init__(self, path): |
|||
package_name = '_jinja2_module_templates_%x' % id(self) |
|||
|
|||
# create a fake module that looks for the templates in the |
|||
# path given. |
|||
mod = _TemplateModule(package_name) |
|||
if isinstance(path, basestring): |
|||
path = [path] |
|||
else: |
|||
path = list(path) |
|||
mod.__path__ = path |
|||
|
|||
sys.modules[package_name] = weakref.proxy(mod, |
|||
lambda x: sys.modules.pop(package_name, None)) |
|||
|
|||
# the only strong reference, the sys.modules entry is weak |
|||
# so that the garbage collector can remove it once the |
|||
# loader that created it goes out of business. |
|||
self.module = mod |
|||
self.package_name = package_name |
|||
|
|||
@staticmethod |
|||
def get_template_key(name): |
|||
return 'tmpl_' + sha1(name.encode('utf-8')).hexdigest() |
|||
|
|||
@staticmethod |
|||
def get_module_filename(name): |
|||
return ModuleLoader.get_template_key(name) + '.py' |
|||
|
|||
@internalcode |
|||
def load(self, environment, name, globals=None): |
|||
key = self.get_template_key(name) |
|||
module = '%s.%s' % (self.package_name, key) |
|||
mod = getattr(self.module, module, None) |
|||
if mod is None: |
|||
try: |
|||
mod = __import__(module, None, None, ['root']) |
|||
except ImportError: |
|||
raise TemplateNotFound(name) |
|||
|
|||
# remove the entry from sys.modules, we only want the attribute |
|||
# on the module object we have stored on the loader. |
|||
sys.modules.pop(module, None) |
|||
|
|||
return environment.template_class.from_module_dict( |
|||
environment, mod.__dict__, globals) |
@ -1,102 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
jinja2.meta |
|||
~~~~~~~~~~~ |
|||
|
|||
This module implements various functions that exposes information about |
|||
templates that might be interesting for various kinds of applications. |
|||
|
|||
:copyright: (c) 2010 by the Jinja Team, see AUTHORS for more details. |
|||
:license: BSD, see LICENSE for more details. |
|||
""" |
|||
from jinja2 import nodes |
|||
from jinja2.compiler import CodeGenerator |
|||
|
|||
|
|||
class TrackingCodeGenerator(CodeGenerator): |
|||
"""We abuse the code generator for introspection.""" |
|||
|
|||
def __init__(self, environment): |
|||
CodeGenerator.__init__(self, environment, '<introspection>', |
|||
'<introspection>') |
|||
self.undeclared_identifiers = set() |
|||
|
|||
def write(self, x): |
|||
"""Don't write.""" |
|||
|
|||
def pull_locals(self, frame): |
|||
"""Remember all undeclared identifiers.""" |
|||
self.undeclared_identifiers.update(frame.identifiers.undeclared) |
|||
|
|||
|
|||
def find_undeclared_variables(ast): |
|||
"""Returns a set of all variables in the AST that will be looked up from |
|||
the context at runtime. Because at compile time it's not known which |
|||
variables will be used depending on the path the execution takes at |
|||
runtime, all variables are returned. |
|||
|
|||
>>> from jinja2 import Environment, meta |
|||
>>> env = Environment() |
|||
>>> ast = env.parse('{% set foo = 42 %}{{ bar + foo }}') |
|||
>>> meta.find_undeclared_variables(ast) |
|||
set(['bar']) |
|||
|
|||
.. admonition:: Implementation |
|||
|
|||
Internally the code generator is used for finding undeclared variables. |
|||
This is good to know because the code generator might raise a |
|||
:exc:`TemplateAssertionError` during compilation and as a matter of |
|||
fact this function can currently raise that exception as well. |
|||
""" |
|||
codegen = TrackingCodeGenerator(ast.environment) |
|||
codegen.visit(ast) |
|||
return codegen.undeclared_identifiers |
|||
|
|||
|
|||
def find_referenced_templates(ast): |
|||
"""Finds all the referenced templates from the AST. This will return an |
|||
iterator over all the hardcoded template extensions, inclusions and |
|||
imports. If dynamic inheritance or inclusion is used, `None` will be |
|||
yielded. |
|||
|
|||
>>> from jinja2 import Environment, meta |
|||
>>> env = Environment() |
|||
>>> ast = env.parse('{% extends "layout.html" %}{% include helper %}') |
|||
>>> list(meta.find_referenced_templates(ast)) |
|||
['layout.html', None] |
|||
|
|||
This function is useful for dependency tracking. For example if you want |
|||
to rebuild parts of the website after a layout template has changed. |
|||
""" |
|||
for node in ast.find_all((nodes.Extends, nodes.FromImport, nodes.Import, |
|||
nodes.Include)): |
|||
if not isinstance(node.template, nodes.Const): |
|||
# a tuple with some non consts in there |
|||
if isinstance(node.template, (nodes.Tuple, nodes.List)): |
|||
for template_name in node.template.items: |
|||
# something const, only yield the strings and ignore |
|||
# non-string consts that really just make no sense |
|||
if isinstance(template_name, nodes.Const): |
|||
if isinstance(template_name.value, basestring): |
|||
yield template_name.value |
|||
# something dynamic in there |
|||
else: |
|||
yield None |
|||
# something dynamic we don't know about here |
|||
else: |
|||
yield None |
|||
continue |
|||
# constant is a basestring, direct template name |
|||
if isinstance(node.template.value, basestring): |
|||
yield node.template.value |
|||
# a tuple or list (latter *should* not happen) made of consts, |
|||
# yield the consts that are strings. We could warn here for |
|||
# non string values |
|||
elif isinstance(node, nodes.Include) and \ |
|||
isinstance(node.template.value, (tuple, list)): |
|||
for template_name in node.template.value: |
|||
if isinstance(template_name, basestring): |
|||
yield template_name |
|||
# something else we don't care about, we could warn here |
|||
else: |
|||
yield None |
@ -1,910 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
jinja2.nodes |
|||
~~~~~~~~~~~~ |
|||
|
|||
This module implements additional nodes derived from the ast base node. |
|||
|
|||
It also provides some node tree helper functions like `in_lineno` and |
|||
`get_nodes` used by the parser and translator in order to normalize |
|||
python and jinja nodes. |
|||
|
|||
:copyright: (c) 2010 by the Jinja Team. |
|||
:license: BSD, see LICENSE for more details. |
|||
""" |
|||
import operator |
|||
from itertools import chain, izip |
|||
from collections import deque |
|||
from jinja2.utils import Markup, MethodType, FunctionType |
|||
|
|||
|
|||
#: the types we support for context functions |
|||
_context_function_types = (FunctionType, MethodType) |
|||
|
|||
|
|||
_binop_to_func = { |
|||
'*': operator.mul, |
|||
'/': operator.truediv, |
|||
'//': operator.floordiv, |
|||
'**': operator.pow, |
|||
'%': operator.mod, |
|||
'+': operator.add, |
|||
'-': operator.sub |
|||
} |
|||
|
|||
_uaop_to_func = { |
|||
'not': operator.not_, |
|||
'+': operator.pos, |
|||
'-': operator.neg |
|||
} |
|||
|
|||
_cmpop_to_func = { |
|||
'eq': operator.eq, |
|||
'ne': operator.ne, |
|||
'gt': operator.gt, |
|||
'gteq': operator.ge, |
|||
'lt': operator.lt, |
|||
'lteq': operator.le, |
|||
'in': lambda a, b: a in b, |
|||
'notin': lambda a, b: a not in b |
|||
} |
|||
|
|||
|
|||
class Impossible(Exception): |
|||
"""Raised if the node could not perform a requested action.""" |
|||
|
|||
|
|||
class NodeType(type): |
|||
"""A metaclass for nodes that handles the field and attribute |
|||
inheritance. fields and attributes from the parent class are |
|||
automatically forwarded to the child.""" |
|||
|
|||
def __new__(cls, name, bases, d): |
|||
for attr in 'fields', 'attributes': |
|||
storage = [] |
|||
storage.extend(getattr(bases[0], attr, ())) |
|||
storage.extend(d.get(attr, ())) |
|||
assert len(bases) == 1, 'multiple inheritance not allowed' |
|||
assert len(storage) == len(set(storage)), 'layout conflict' |
|||
d[attr] = tuple(storage) |
|||
d.setdefault('abstract', False) |
|||
return type.__new__(cls, name, bases, d) |
|||
|
|||
|
|||
class EvalContext(object): |
|||
"""Holds evaluation time information. Custom attributes can be attached |
|||
to it in extensions. |
|||
""" |
|||
|
|||
def __init__(self, environment, template_name=None): |
|||
self.environment = environment |
|||
if callable(environment.autoescape): |
|||
self.autoescape = environment.autoescape(template_name) |
|||
else: |
|||
self.autoescape = environment.autoescape |
|||
self.volatile = False |
|||
|
|||
def save(self): |
|||
return self.__dict__.copy() |
|||
|
|||
def revert(self, old): |
|||
self.__dict__.clear() |
|||
self.__dict__.update(old) |
|||
|
|||
|
|||
def get_eval_context(node, ctx): |
|||
if ctx is None: |
|||
if node.environment is None: |
|||
raise RuntimeError('if no eval context is passed, the ' |
|||
'node must have an attached ' |
|||
'environment.') |
|||
return EvalContext(node.environment) |
|||
return ctx |
|||
|
|||
|
|||
class Node(object): |
|||
"""Baseclass for all Jinja2 nodes. There are a number of nodes available |
|||
of different types. There are three major types: |
|||
|
|||
- :class:`Stmt`: statements |
|||
- :class:`Expr`: expressions |
|||
- :class:`Helper`: helper nodes |
|||
- :class:`Template`: the outermost wrapper node |
|||
|
|||
All nodes have fields and attributes. Fields may be other nodes, lists, |
|||
or arbitrary values. Fields are passed to the constructor as regular |
|||
positional arguments, attributes as keyword arguments. Each node has |
|||
two attributes: `lineno` (the line number of the node) and `environment`. |
|||
The `environment` attribute is set at the end of the parsing process for |
|||
all nodes automatically. |
|||
""" |
|||
__metaclass__ = NodeType |
|||
fields = () |
|||
attributes = ('lineno', 'environment') |
|||
abstract = True |
|||
|
|||
def __init__(self, *fields, **attributes): |
|||
if self.abstract: |
|||
raise TypeError('abstract nodes are not instanciable') |
|||
if fields: |
|||
if len(fields) != len(self.fields): |
|||
if not self.fields: |
|||
raise TypeError('%r takes 0 arguments' % |
|||
self.__class__.__name__) |
|||
raise TypeError('%r takes 0 or %d argument%s' % ( |
|||
self.__class__.__name__, |
|||
len(self.fields), |
|||
len(self.fields) != 1 and 's' or '' |
|||
)) |
|||
for name, arg in izip(self.fields, fields): |
|||
setattr(self, name, arg) |
|||
for attr in self.attributes: |
|||
setattr(self, attr, attributes.pop(attr, None)) |
|||
if attributes: |
|||
raise TypeError('unknown attribute %r' % |
|||
iter(attributes).next()) |
|||
|
|||
def iter_fields(self, exclude=None, only=None): |
|||
"""This method iterates over all fields that are defined and yields |
|||
``(key, value)`` tuples. Per default all fields are returned, but |
|||
it's possible to limit that to some fields by providing the `only` |
|||
parameter or to exclude some using the `exclude` parameter. Both |
|||
should be sets or tuples of field names. |
|||
""" |
|||
for name in self.fields: |
|||
if (exclude is only is None) or \ |
|||
(exclude is not None and name not in exclude) or \ |
|||
(only is not None and name in only): |
|||
try: |
|||
yield name, getattr(self, name) |
|||
except AttributeError: |
|||
pass |
|||
|
|||
def iter_child_nodes(self, exclude=None, only=None): |
|||
"""Iterates over all direct child nodes of the node. This iterates |
|||
over all fields and yields the values of they are nodes. If the value |
|||
of a field is a list all the nodes in that list are returned. |
|||
""" |
|||
for field, item in self.iter_fields(exclude, only): |
|||
if isinstance(item, list): |
|||
for n in item: |
|||
if isinstance(n, Node): |
|||
yield n |
|||
elif isinstance(item, Node): |
|||
yield item |
|||
|
|||
def find(self, node_type): |
|||
"""Find the first node of a given type. If no such node exists the |
|||
return value is `None`. |
|||
""" |
|||
for result in self.find_all(node_type): |
|||
return result |
|||
|
|||
def find_all(self, node_type): |
|||
"""Find all the nodes of a given type. If the type is a tuple, |
|||
the check is performed for any of the tuple items. |
|||
""" |
|||
for child in self.iter_child_nodes(): |
|||
if isinstance(child, node_type): |
|||
yield child |
|||
for result in child.find_all(node_type): |
|||
yield result |
|||
|
|||
def set_ctx(self, ctx): |
|||
"""Reset the context of a node and all child nodes. Per default the |
|||
parser will all generate nodes that have a 'load' context as it's the |
|||
most common one. This method is used in the parser to set assignment |
|||
targets and other nodes to a store context. |
|||
""" |
|||
todo = deque([self]) |
|||
while todo: |
|||
node = todo.popleft() |
|||
if 'ctx' in node.fields: |
|||
node.ctx = ctx |
|||
todo.extend(node.iter_child_nodes()) |
|||
return self |
|||
|
|||
def set_lineno(self, lineno, override=False): |
|||
"""Set the line numbers of the node and children.""" |
|||
todo = deque([self]) |
|||
while todo: |
|||
node = todo.popleft() |
|||
if 'lineno' in node.attributes: |
|||
if node.lineno is None or override: |
|||
node.lineno = lineno |
|||
todo.extend(node.iter_child_nodes()) |
|||
return self |
|||
|
|||
def set_environment(self, environment): |
|||
"""Set the environment for all nodes.""" |
|||
todo = deque([self]) |
|||
while todo: |
|||
node = todo.popleft() |
|||
node.environment = environment |
|||
todo.extend(node.iter_child_nodes()) |
|||
return self |
|||
|
|||
def __eq__(self, other): |
|||
return type(self) is type(other) and \ |
|||
tuple(self.iter_fields()) == tuple(other.iter_fields()) |
|||
|
|||
def __ne__(self, other): |
|||
return not self.__eq__(other) |
|||
|
|||
def __repr__(self): |
|||
return '%s(%s)' % ( |
|||
self.__class__.__name__, |
|||
', '.join('%s=%r' % (arg, getattr(self, arg, None)) for |
|||
arg in self.fields) |
|||
) |
|||
|
|||
|
|||
class Stmt(Node): |
|||
"""Base node for all statements.""" |
|||
abstract = True |
|||
|
|||
|
|||
class Helper(Node): |
|||
"""Nodes that exist in a specific context only.""" |
|||
abstract = True |
|||
|
|||
|
|||
class Template(Node): |
|||
"""Node that represents a template. This must be the outermost node that |
|||
is passed to the compiler. |
|||
""" |
|||
fields = ('body',) |
|||
|
|||
|
|||
class Output(Stmt): |
|||
"""A node that holds multiple expressions which are then printed out. |
|||
This is used both for the `print` statement and the regular template data. |
|||
""" |
|||
fields = ('nodes',) |
|||
|
|||
|
|||
class Extends(Stmt): |
|||
"""Represents an extends statement.""" |
|||
fields = ('template',) |
|||
|
|||
|
|||
class For(Stmt): |
|||
"""The for loop. `target` is the target for the iteration (usually a |
|||
:class:`Name` or :class:`Tuple`), `iter` the iterable. `body` is a list |
|||
of nodes that are used as loop-body, and `else_` a list of nodes for the |
|||
`else` block. If no else node exists it has to be an empty list. |
|||
|
|||
For filtered nodes an expression can be stored as `test`, otherwise `None`. |
|||
""" |
|||
fields = ('target', 'iter', 'body', 'else_', 'test', 'recursive') |
|||
|
|||
|
|||
class If(Stmt): |
|||
"""If `test` is true, `body` is rendered, else `else_`.""" |
|||
fields = ('test', 'body', 'else_') |
|||
|
|||
|
|||
class Macro(Stmt): |
|||
"""A macro definition. `name` is the name of the macro, `args` a list of |
|||
arguments and `defaults` a list of defaults if there are any. `body` is |
|||
a list of nodes for the macro body. |
|||
""" |
|||
fields = ('name', 'args', 'defaults', 'body') |
|||
|
|||
|
|||
class CallBlock(Stmt): |
|||
"""Like a macro without a name but a call instead. `call` is called with |
|||
the unnamed macro as `caller` argument this node holds. |
|||
""" |
|||
fields = ('call', 'args', 'defaults', 'body') |
|||
|
|||
|
|||
class FilterBlock(Stmt): |
|||
"""Node for filter sections.""" |
|||
fields = ('body', 'filter') |
|||
|
|||
|
|||
class Block(Stmt): |
|||
"""A node that represents a block.""" |
|||
fields = ('name', 'body', 'scoped') |
|||
|
|||
|
|||
class Include(Stmt): |
|||
"""A node that represents the include tag.""" |
|||
fields = ('template', 'with_context', 'ignore_missing') |
|||
|
|||
|
|||
class Import(Stmt): |
|||
"""A node that represents the import tag.""" |
|||
fields = ('template', 'target', 'with_context') |
|||
|
|||
|
|||
class FromImport(Stmt): |
|||
"""A node that represents the from import tag. It's important to not |
|||
pass unsafe names to the name attribute. The compiler translates the |
|||
attribute lookups directly into getattr calls and does *not* use the |
|||
subscript callback of the interface. As exported variables may not |
|||
start with double underscores (which the parser asserts) this is not a |
|||
problem for regular Jinja code, but if this node is used in an extension |
|||
extra care must be taken. |
|||
|
|||
The list of names may contain tuples if aliases are wanted. |
|||
""" |
|||
fields = ('template', 'names', 'with_context') |
|||
|
|||
|
|||
class ExprStmt(Stmt): |
|||
"""A statement that evaluates an expression and discards the result.""" |
|||
fields = ('node',) |
|||
|
|||
|
|||
class Assign(Stmt): |
|||
"""Assigns an expression to a target.""" |
|||
fields = ('target', 'node') |
|||
|
|||
|
|||
class Expr(Node): |
|||
"""Baseclass for all expressions.""" |
|||
abstract = True |
|||
|
|||
def as_const(self, eval_ctx=None): |
|||
"""Return the value of the expression as constant or raise |
|||
:exc:`Impossible` if this was not possible. |
|||
|
|||
An :class:`EvalContext` can be provided, if none is given |
|||
a default context is created which requires the nodes to have |
|||
an attached environment. |
|||
|
|||
.. versionchanged:: 2.4 |
|||
the `eval_ctx` parameter was added. |
|||
""" |
|||
raise Impossible() |
|||
|
|||
def can_assign(self): |
|||
"""Check if it's possible to assign something to this node.""" |
|||
return False |
|||
|
|||
|
|||
class BinExpr(Expr): |
|||
"""Baseclass for all binary expressions.""" |
|||
fields = ('left', 'right') |
|||
operator = None |
|||
abstract = True |
|||
|
|||
def as_const(self, eval_ctx=None): |
|||
eval_ctx = get_eval_context(self, eval_ctx) |
|||
# intercepted operators cannot be folded at compile time |
|||
if self.environment.sandboxed and \ |
|||
self.operator in self.environment.intercepted_binops: |
|||
raise Impossible() |
|||
f = _binop_to_func[self.operator] |
|||
try: |
|||
return f(self.left.as_const(eval_ctx), self.right.as_const(eval_ctx)) |
|||
except Exception: |
|||
raise Impossible() |
|||
|
|||
|
|||
class UnaryExpr(Expr): |
|||
"""Baseclass for all unary expressions.""" |
|||
fields = ('node',) |
|||
operator = None |
|||
abstract = True |
|||
|
|||
def as_const(self, eval_ctx=None): |
|||
eval_ctx = get_eval_context(self, eval_ctx) |
|||
# intercepted operators cannot be folded at compile time |
|||
if self.environment.sandboxed and \ |
|||
self.operator in self.environment.intercepted_unops: |
|||
raise Impossible() |
|||
f = _uaop_to_func[self.operator] |
|||
try: |
|||
return f(self.node.as_const(eval_ctx)) |
|||
except Exception: |
|||
raise Impossible() |
|||
|
|||
|
|||
class Name(Expr): |
|||
"""Looks up a name or stores a value in a name. |
|||
The `ctx` of the node can be one of the following values: |
|||
|
|||
- `store`: store a value in the name |
|||
- `load`: load that name |
|||
- `param`: like `store` but if the name was defined as function parameter. |
|||
""" |
|||
fields = ('name', 'ctx') |
|||
|
|||
def can_assign(self): |
|||
return self.name not in ('true', 'false', 'none', |
|||
'True', 'False', 'None') |
|||
|
|||
|
|||
class Literal(Expr): |
|||
"""Baseclass for literals.""" |
|||
abstract = True |
|||
|
|||
|
|||
class Const(Literal): |
|||
"""All constant values. The parser will return this node for simple |
|||
constants such as ``42`` or ``"foo"`` but it can be used to store more |
|||
complex values such as lists too. Only constants with a safe |
|||
representation (objects where ``eval(repr(x)) == x`` is true). |
|||
""" |
|||
fields = ('value',) |
|||
|
|||
def as_const(self, eval_ctx=None): |
|||
return self.value |
|||
|
|||
@classmethod |
|||
def from_untrusted(cls, value, lineno=None, environment=None): |
|||
"""Return a const object if the value is representable as |
|||
constant value in the generated code, otherwise it will raise |
|||
an `Impossible` exception. |
|||
""" |
|||
from compiler import has_safe_repr |
|||
if not has_safe_repr(value): |
|||
raise Impossible() |
|||
return cls(value, lineno=lineno, environment=environment) |
|||
|
|||
|
|||
class TemplateData(Literal): |
|||
"""A constant template string.""" |
|||
fields = ('data',) |
|||
|
|||
def as_const(self, eval_ctx=None): |
|||
eval_ctx = get_eval_context(self, eval_ctx) |
|||
if eval_ctx.volatile: |
|||
raise Impossible() |
|||
if eval_ctx.autoescape: |
|||
return Markup(self.data) |
|||
return self.data |
|||
|
|||
|
|||
class Tuple(Literal): |
|||
"""For loop unpacking and some other things like multiple arguments |
|||
for subscripts. Like for :class:`Name` `ctx` specifies if the tuple |
|||
is used for loading the names or storing. |
|||
""" |
|||
fields = ('items', 'ctx') |
|||
|
|||
def as_const(self, eval_ctx=None): |
|||
eval_ctx = get_eval_context(self, eval_ctx) |
|||
return tuple(x.as_const(eval_ctx) for x in self.items) |
|||
|
|||
def can_assign(self): |
|||
for item in self.items: |
|||
if not item.can_assign(): |
|||
return False |
|||
return True |
|||
|
|||
|
|||
class List(Literal): |
|||
"""Any list literal such as ``[1, 2, 3]``""" |
|||
fields = ('items',) |
|||
|
|||
def as_const(self, eval_ctx=None): |
|||
eval_ctx = get_eval_context(self, eval_ctx) |
|||
return [x.as_const(eval_ctx) for x in self.items] |
|||
|
|||
|
|||
class Dict(Literal): |
|||
"""Any dict literal such as ``{1: 2, 3: 4}``. The items must be a list of |
|||
:class:`Pair` nodes. |
|||
""" |
|||
fields = ('items',) |
|||
|
|||
def as_const(self, eval_ctx=None): |
|||
eval_ctx = get_eval_context(self, eval_ctx) |
|||
return dict(x.as_const(eval_ctx) for x in self.items) |
|||
|
|||
|
|||
class Pair(Helper): |
|||
"""A key, value pair for dicts.""" |
|||
fields = ('key', 'value') |
|||
|
|||
def as_const(self, eval_ctx=None): |
|||
eval_ctx = get_eval_context(self, eval_ctx) |
|||
return self.key.as_const(eval_ctx), self.value.as_const(eval_ctx) |
|||
|
|||
|
|||
class Keyword(Helper): |
|||
"""A key, value pair for keyword arguments where key is a string.""" |
|||
fields = ('key', 'value') |
|||
|
|||
def as_const(self, eval_ctx=None): |
|||
eval_ctx = get_eval_context(self, eval_ctx) |
|||
return self.key, self.value.as_const(eval_ctx) |
|||
|
|||
|
|||
class CondExpr(Expr): |
|||
"""A conditional expression (inline if expression). (``{{ |
|||
foo if bar else baz }}``) |
|||
""" |
|||
fields = ('test', 'expr1', 'expr2') |
|||
|
|||
def as_const(self, eval_ctx=None): |
|||
eval_ctx = get_eval_context(self, eval_ctx) |
|||
if self.test.as_const(eval_ctx): |
|||
return self.expr1.as_const(eval_ctx) |
|||
|
|||
# if we evaluate to an undefined object, we better do that at runtime |
|||
if self.expr2 is None: |
|||
raise Impossible() |
|||
|
|||
return self.expr2.as_const(eval_ctx) |
|||
|
|||
|
|||
class Filter(Expr): |
|||
"""This node applies a filter on an expression. `name` is the name of |
|||
the filter, the rest of the fields are the same as for :class:`Call`. |
|||
|
|||
If the `node` of a filter is `None` the contents of the last buffer are |
|||
filtered. Buffers are created by macros and filter blocks. |
|||
""" |
|||
fields = ('node', 'name', 'args', 'kwargs', 'dyn_args', 'dyn_kwargs') |
|||
|
|||
def as_const(self, eval_ctx=None): |
|||
eval_ctx = get_eval_context(self, eval_ctx) |
|||
if eval_ctx.volatile or self.node is None: |
|||
raise Impossible() |
|||
# we have to be careful here because we call filter_ below. |
|||
# if this variable would be called filter, 2to3 would wrap the |
|||
# call in a list beause it is assuming we are talking about the |
|||
# builtin filter function here which no longer returns a list in |
|||
# python 3. because of that, do not rename filter_ to filter! |
|||
filter_ = self.environment.filters.get(self.name) |
|||
if filter_ is None or getattr(filter_, 'contextfilter', False): |
|||
raise Impossible() |
|||
obj = self.node.as_const(eval_ctx) |
|||
args = [x.as_const(eval_ctx) for x in self.args] |
|||
if getattr(filter_, 'evalcontextfilter', False): |
|||
args.insert(0, eval_ctx) |
|||
elif getattr(filter_, 'environmentfilter', False): |
|||
args.insert(0, self.environment) |
|||
kwargs = dict(x.as_const(eval_ctx) for x in self.kwargs) |
|||
if self.dyn_args is not None: |
|||
try: |
|||
args.extend(self.dyn_args.as_const(eval_ctx)) |
|||
except Exception: |
|||
raise Impossible() |
|||
if self.dyn_kwargs is not None: |
|||
try: |
|||
kwargs.update(self.dyn_kwargs.as_const(eval_ctx)) |
|||
except Exception: |
|||
raise Impossible() |
|||
try: |
|||
return filter_(obj, *args, **kwargs) |
|||
except Exception: |
|||
raise Impossible() |
|||
|
|||
|
|||
class Test(Expr): |
|||
"""Applies a test on an expression. `name` is the name of the test, the |
|||
rest of the fields are the same as for :class:`Call`. |
|||
""" |
|||
fields = ('node', 'name', 'args', 'kwargs', 'dyn_args', 'dyn_kwargs') |
|||
|
|||
|
|||
class Call(Expr): |
|||
"""Calls an expression. `args` is a list of arguments, `kwargs` a list |
|||
of keyword arguments (list of :class:`Keyword` nodes), and `dyn_args` |
|||
and `dyn_kwargs` has to be either `None` or a node that is used as |
|||
node for dynamic positional (``*args``) or keyword (``**kwargs``) |
|||
arguments. |
|||
""" |
|||
fields = ('node', 'args', 'kwargs', 'dyn_args', 'dyn_kwargs') |
|||
|
|||
def as_const(self, eval_ctx=None): |
|||
eval_ctx = get_eval_context(self, eval_ctx) |
|||
if eval_ctx.volatile: |
|||
raise Impossible() |
|||
obj = self.node.as_const(eval_ctx) |
|||
|
|||
# don't evaluate context functions |
|||
args = [x.as_const(eval_ctx) for x in self.args] |
|||
if isinstance(obj, _context_function_types): |
|||
if getattr(obj, 'contextfunction', False): |
|||
raise Impossible() |
|||
elif getattr(obj, 'evalcontextfunction', False): |
|||
args.insert(0, eval_ctx) |
|||
elif getattr(obj, 'environmentfunction', False): |
|||
args.insert(0, self.environment) |
|||
|
|||
kwargs = dict(x.as_const(eval_ctx) for x in self.kwargs) |
|||
if self.dyn_args is not None: |
|||
try: |
|||
args.extend(self.dyn_args.as_const(eval_ctx)) |
|||
except Exception: |
|||
raise Impossible() |
|||
if self.dyn_kwargs is not None: |
|||
try: |
|||
kwargs.update(self.dyn_kwargs.as_const(eval_ctx)) |
|||
except Exception: |
|||
raise Impossible() |
|||
try: |
|||
return obj(*args, **kwargs) |
|||
except Exception: |
|||
raise Impossible() |
|||
|
|||
|
|||
class Getitem(Expr): |
|||
"""Get an attribute or item from an expression and prefer the item.""" |
|||
fields = ('node', 'arg', 'ctx') |
|||
|
|||
def as_const(self, eval_ctx=None): |
|||
eval_ctx = get_eval_context(self, eval_ctx) |
|||
if self.ctx != 'load': |
|||
raise Impossible() |
|||
try: |
|||
return self.environment.getitem(self.node.as_const(eval_ctx), |
|||
self.arg.as_const(eval_ctx)) |
|||
except Exception: |
|||
raise Impossible() |
|||
|
|||
def can_assign(self): |
|||
return False |
|||
|
|||
|
|||
class Getattr(Expr): |
|||
"""Get an attribute or item from an expression that is a ascii-only |
|||
bytestring and prefer the attribute. |
|||
""" |
|||
fields = ('node', 'attr', 'ctx') |
|||
|
|||
def as_const(self, eval_ctx=None): |
|||
if self.ctx != 'load': |
|||
raise Impossible() |
|||
try: |
|||
eval_ctx = get_eval_context(self, eval_ctx) |
|||
return self.environment.getattr(self.node.as_const(eval_ctx), |
|||
self.attr) |
|||
except Exception: |
|||
raise Impossible() |
|||
|
|||
def can_assign(self): |
|||
return False |
|||
|
|||
|
|||
class Slice(Expr): |
|||
"""Represents a slice object. This must only be used as argument for |
|||
:class:`Subscript`. |
|||
""" |
|||
fields = ('start', 'stop', 'step') |
|||
|
|||
def as_const(self, eval_ctx=None): |
|||
eval_ctx = get_eval_context(self, eval_ctx) |
|||
def const(obj): |
|||
if obj is None: |
|||
return None |
|||
return obj.as_const(eval_ctx) |
|||
return slice(const(self.start), const(self.stop), const(self.step)) |
|||
|
|||
|
|||
class Concat(Expr): |
|||
"""Concatenates the list of expressions provided after converting them to |
|||
unicode. |
|||
""" |
|||
fields = ('nodes',) |
|||
|
|||
def as_const(self, eval_ctx=None): |
|||
eval_ctx = get_eval_context(self, eval_ctx) |
|||
return ''.join(unicode(x.as_const(eval_ctx)) for x in self.nodes) |
|||
|
|||
|
|||
class Compare(Expr): |
|||
"""Compares an expression with some other expressions. `ops` must be a |
|||
list of :class:`Operand`\s. |
|||
""" |
|||
fields = ('expr', 'ops') |
|||
|
|||
def as_const(self, eval_ctx=None): |
|||
eval_ctx = get_eval_context(self, eval_ctx) |
|||
result = value = self.expr.as_const(eval_ctx) |
|||
try: |
|||
for op in self.ops: |
|||
new_value = op.expr.as_const(eval_ctx) |
|||
result = _cmpop_to_func[op.op](value, new_value) |
|||
value = new_value |
|||
except Exception: |
|||
raise Impossible() |
|||
return result |
|||
|
|||
|
|||
class Operand(Helper): |
|||
"""Holds an operator and an expression.""" |
|||
fields = ('op', 'expr') |
|||
|
|||
if __debug__: |
|||
Operand.__doc__ += '\nThe following operators are available: ' + \ |
|||
', '.join(sorted('``%s``' % x for x in set(_binop_to_func) | |
|||
set(_uaop_to_func) | set(_cmpop_to_func))) |
|||
|
|||
|
|||
class Mul(BinExpr): |
|||
"""Multiplies the left with the right node.""" |
|||
operator = '*' |
|||
|
|||
|
|||
class Div(BinExpr): |
|||
"""Divides the left by the right node.""" |
|||
operator = '/' |
|||
|
|||
|
|||
class FloorDiv(BinExpr): |
|||
"""Divides the left by the right node and truncates conver the |
|||
result into an integer by truncating. |
|||
""" |
|||
operator = '//' |
|||
|
|||
|
|||
class Add(BinExpr): |
|||
"""Add the left to the right node.""" |
|||
operator = '+' |
|||
|
|||
|
|||
class Sub(BinExpr): |
|||
"""Substract the right from the left node.""" |
|||
operator = '-' |
|||
|
|||
|
|||
class Mod(BinExpr): |
|||
"""Left modulo right.""" |
|||
operator = '%' |
|||
|
|||
|
|||
class Pow(BinExpr): |
|||
"""Left to the power of right.""" |
|||
operator = '**' |
|||
|
|||
|
|||
class And(BinExpr): |
|||
"""Short circuited AND.""" |
|||
operator = 'and' |
|||
|
|||
def as_const(self, eval_ctx=None): |
|||
eval_ctx = get_eval_context(self, eval_ctx) |
|||
return self.left.as_const(eval_ctx) and self.right.as_const(eval_ctx) |
|||
|
|||
|
|||
class Or(BinExpr): |
|||
"""Short circuited OR.""" |
|||
operator = 'or' |
|||
|
|||
def as_const(self, eval_ctx=None): |
|||
eval_ctx = get_eval_context(self, eval_ctx) |
|||
return self.left.as_const(eval_ctx) or self.right.as_const(eval_ctx) |
|||
|
|||
|
|||
class Not(UnaryExpr): |
|||
"""Negate the expression.""" |
|||
operator = 'not' |
|||
|
|||
|
|||
class Neg(UnaryExpr): |
|||
"""Make the expression negative.""" |
|||
operator = '-' |
|||
|
|||
|
|||
class Pos(UnaryExpr): |
|||
"""Make the expression positive (noop for most expressions)""" |
|||
operator = '+' |
|||
|
|||
|
|||
# Helpers for extensions |
|||
|
|||
|
|||
class EnvironmentAttribute(Expr): |
|||
"""Loads an attribute from the environment object. This is useful for |
|||
extensions that want to call a callback stored on the environment. |
|||
""" |
|||
fields = ('name',) |
|||
|
|||
|
|||
class ExtensionAttribute(Expr): |
|||
"""Returns the attribute of an extension bound to the environment. |
|||
The identifier is the identifier of the :class:`Extension`. |
|||
|
|||
This node is usually constructed by calling the |
|||
:meth:`~jinja2.ext.Extension.attr` method on an extension. |
|||
""" |
|||
fields = ('identifier', 'name') |
|||
|
|||
|
|||
class ImportedName(Expr): |
|||
"""If created with an import name the import name is returned on node |
|||
access. For example ``ImportedName('cgi.escape')`` returns the `escape` |
|||
function from the cgi module on evaluation. Imports are optimized by the |
|||
compiler so there is no need to assign them to local variables. |
|||
""" |
|||
fields = ('importname',) |
|||
|
|||
|
|||
class InternalName(Expr): |
|||
"""An internal name in the compiler. You cannot create these nodes |
|||
yourself but the parser provides a |
|||
:meth:`~jinja2.parser.Parser.free_identifier` method that creates |
|||
a new identifier for you. This identifier is not available from the |
|||
template and is not threated specially by the compiler. |
|||
""" |
|||
fields = ('name',) |
|||
|
|||
def __init__(self): |
|||
raise TypeError('Can\'t create internal names. Use the ' |
|||
'`free_identifier` method on a parser.') |
|||
|
|||
|
|||
class MarkSafe(Expr): |
|||
"""Mark the wrapped expression as safe (wrap it as `Markup`).""" |
|||
fields = ('expr',) |
|||
|
|||
def as_const(self, eval_ctx=None): |
|||
eval_ctx = get_eval_context(self, eval_ctx) |
|||
return Markup(self.expr.as_const(eval_ctx)) |
|||
|
|||
|
|||
class MarkSafeIfAutoescape(Expr): |
|||
"""Mark the wrapped expression as safe (wrap it as `Markup`) but |
|||
only if autoescaping is active. |
|||
|
|||
.. versionadded:: 2.5 |
|||
""" |
|||
fields = ('expr',) |
|||
|
|||
def as_const(self, eval_ctx=None): |
|||
eval_ctx = get_eval_context(self, eval_ctx) |
|||
if eval_ctx.volatile: |
|||
raise Impossible() |
|||
expr = self.expr.as_const(eval_ctx) |
|||
if eval_ctx.autoescape: |
|||
return Markup(expr) |
|||
return expr |
|||
|
|||
|
|||
class ContextReference(Expr): |
|||
"""Returns the current template context. It can be used like a |
|||
:class:`Name` node, with a ``'load'`` ctx and will return the |
|||
current :class:`~jinja2.runtime.Context` object. |
|||
|
|||
Here an example that assigns the current template name to a |
|||
variable named `foo`:: |
|||
|
|||
Assign(Name('foo', ctx='store'), |
|||
Getattr(ContextReference(), 'name')) |
|||
""" |
|||
|
|||
|
|||
class Continue(Stmt): |
|||
"""Continue a loop.""" |
|||
|
|||
|
|||
class Break(Stmt): |
|||
"""Break a loop.""" |
|||
|
|||
|
|||
class Scope(Stmt): |
|||
"""An artificial scope.""" |
|||
fields = ('body',) |
|||
|
|||
|
|||
class EvalContextModifier(Stmt): |
|||
"""Modifies the eval context. For each option that should be modified, |
|||
a :class:`Keyword` has to be added to the :attr:`options` list. |
|||
|
|||
Example to change the `autoescape` setting:: |
|||
|
|||
EvalContextModifier(options=[Keyword('autoescape', Const(True))]) |
|||
""" |
|||
fields = ('options',) |
|||
|
|||
|
|||
class ScopedEvalContextModifier(EvalContextModifier): |
|||
"""Modifies the eval context and reverts it later. Works exactly like |
|||
:class:`EvalContextModifier` but will only modify the |
|||
:class:`~jinja2.nodes.EvalContext` for nodes in the :attr:`body`. |
|||
""" |
|||
fields = ('body',) |
|||
|
|||
|
|||
# make sure nobody creates custom nodes |
|||
def _failing_new(*args, **kwargs): |
|||
raise TypeError('can\'t create custom node types') |
|||
NodeType.__new__ = staticmethod(_failing_new); del _failing_new |
@ -1,68 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
jinja2.optimizer |
|||
~~~~~~~~~~~~~~~~ |
|||
|
|||
The jinja optimizer is currently trying to constant fold a few expressions |
|||
and modify the AST in place so that it should be easier to evaluate it. |
|||
|
|||
Because the AST does not contain all the scoping information and the |
|||
compiler has to find that out, we cannot do all the optimizations we |
|||
want. For example loop unrolling doesn't work because unrolled loops would |
|||
have a different scoping. |
|||
|
|||
The solution would be a second syntax tree that has the scoping rules stored. |
|||
|
|||
:copyright: (c) 2010 by the Jinja Team. |
|||
:license: BSD. |
|||
""" |
|||
from jinja2 import nodes |
|||
from jinja2.visitor import NodeTransformer |
|||
|
|||
|
|||
def optimize(node, environment): |
|||
"""The context hint can be used to perform an static optimization |
|||
based on the context given.""" |
|||
optimizer = Optimizer(environment) |
|||
return optimizer.visit(node) |
|||
|
|||
|
|||
class Optimizer(NodeTransformer): |
|||
|
|||
def __init__(self, environment): |
|||
self.environment = environment |
|||
|
|||
def visit_If(self, node): |
|||
"""Eliminate dead code.""" |
|||
# do not optimize ifs that have a block inside so that it doesn't |
|||
# break super(). |
|||
if node.find(nodes.Block) is not None: |
|||
return self.generic_visit(node) |
|||
try: |
|||
val = self.visit(node.test).as_const() |
|||
except nodes.Impossible: |
|||
return self.generic_visit(node) |
|||
if val: |
|||
body = node.body |
|||
else: |
|||
body = node.else_ |
|||
result = [] |
|||
for node in body: |
|||
result.extend(self.visit_list(node)) |
|||
return result |
|||
|
|||
def fold(self, node): |
|||
"""Do constant folding.""" |
|||
node = self.generic_visit(node) |
|||
try: |
|||
return nodes.Const.from_untrusted(node.as_const(), |
|||
lineno=node.lineno, |
|||
environment=self.environment) |
|||
except nodes.Impossible: |
|||
return node |
|||
|
|||
visit_Add = visit_Sub = visit_Mul = visit_Div = visit_FloorDiv = \ |
|||
visit_Pow = visit_Mod = visit_And = visit_Or = visit_Pos = visit_Neg = \ |
|||
visit_Not = visit_Compare = visit_Getitem = visit_Getattr = visit_Call = \ |
|||
visit_Filter = visit_Test = visit_CondExpr = fold |
|||
del fold |
@ -1,896 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
jinja2.parser |
|||
~~~~~~~~~~~~~ |
|||
|
|||
Implements the template parser. |
|||
|
|||
:copyright: (c) 2010 by the Jinja Team. |
|||
:license: BSD, see LICENSE for more details. |
|||
""" |
|||
from jinja2 import nodes |
|||
from jinja2.exceptions import TemplateSyntaxError, TemplateAssertionError |
|||
from jinja2.utils import next |
|||
from jinja2.lexer import describe_token, describe_token_expr |
|||
|
|||
|
|||
#: statements that callinto |
|||
_statement_keywords = frozenset(['for', 'if', 'block', 'extends', 'print', |
|||
'macro', 'include', 'from', 'import', |
|||
'set']) |
|||
_compare_operators = frozenset(['eq', 'ne', 'lt', 'lteq', 'gt', 'gteq']) |
|||
|
|||
|
|||
class Parser(object): |
|||
"""This is the central parsing class Jinja2 uses. It's passed to |
|||
extensions and can be used to parse expressions or statements. |
|||
""" |
|||
|
|||
def __init__(self, environment, source, name=None, filename=None, |
|||
state=None): |
|||
self.environment = environment |
|||
self.stream = environment._tokenize(source, name, filename, state) |
|||
self.name = name |
|||
self.filename = filename |
|||
self.closed = False |
|||
self.extensions = {} |
|||
for extension in environment.iter_extensions(): |
|||
for tag in extension.tags: |
|||
self.extensions[tag] = extension.parse |
|||
self._last_identifier = 0 |
|||
self._tag_stack = [] |
|||
self._end_token_stack = [] |
|||
|
|||
def fail(self, msg, lineno=None, exc=TemplateSyntaxError): |
|||
"""Convenience method that raises `exc` with the message, passed |
|||
line number or last line number as well as the current name and |
|||
filename. |
|||
""" |
|||
if lineno is None: |
|||
lineno = self.stream.current.lineno |
|||
raise exc(msg, lineno, self.name, self.filename) |
|||
|
|||
def _fail_ut_eof(self, name, end_token_stack, lineno): |
|||
expected = [] |
|||
for exprs in end_token_stack: |
|||
expected.extend(map(describe_token_expr, exprs)) |
|||
if end_token_stack: |
|||
currently_looking = ' or '.join( |
|||
"'%s'" % describe_token_expr(expr) |
|||
for expr in end_token_stack[-1]) |
|||
else: |
|||
currently_looking = None |
|||
|
|||
if name is None: |
|||
message = ['Unexpected end of template.'] |
|||
else: |
|||
message = ['Encountered unknown tag \'%s\'.' % name] |
|||
|
|||
if currently_looking: |
|||
if name is not None and name in expected: |
|||
message.append('You probably made a nesting mistake. Jinja ' |
|||
'is expecting this tag, but currently looking ' |
|||
'for %s.' % currently_looking) |
|||
else: |
|||
message.append('Jinja was looking for the following tags: ' |
|||
'%s.' % currently_looking) |
|||
|
|||
if self._tag_stack: |
|||
message.append('The innermost block that needs to be ' |
|||
'closed is \'%s\'.' % self._tag_stack[-1]) |
|||
|
|||
self.fail(' '.join(message), lineno) |
|||
|
|||
def fail_unknown_tag(self, name, lineno=None): |
|||
"""Called if the parser encounters an unknown tag. Tries to fail |
|||
with a human readable error message that could help to identify |
|||
the problem. |
|||
""" |
|||
return self._fail_ut_eof(name, self._end_token_stack, lineno) |
|||
|
|||
def fail_eof(self, end_tokens=None, lineno=None): |
|||
"""Like fail_unknown_tag but for end of template situations.""" |
|||
stack = list(self._end_token_stack) |
|||
if end_tokens is not None: |
|||
stack.append(end_tokens) |
|||
return self._fail_ut_eof(None, stack, lineno) |
|||
|
|||
def is_tuple_end(self, extra_end_rules=None): |
|||
"""Are we at the end of a tuple?""" |
|||
if self.stream.current.type in ('variable_end', 'block_end', 'rparen'): |
|||
return True |
|||
elif extra_end_rules is not None: |
|||
return self.stream.current.test_any(extra_end_rules) |
|||
return False |
|||
|
|||
def free_identifier(self, lineno=None): |
|||
"""Return a new free identifier as :class:`~jinja2.nodes.InternalName`.""" |
|||
self._last_identifier += 1 |
|||
rv = object.__new__(nodes.InternalName) |
|||
nodes.Node.__init__(rv, 'fi%d' % self._last_identifier, lineno=lineno) |
|||
return rv |
|||
|
|||
def parse_statement(self): |
|||
"""Parse a single statement.""" |
|||
token = self.stream.current |
|||
if token.type != 'name': |
|||
self.fail('tag name expected', token.lineno) |
|||
self._tag_stack.append(token.value) |
|||
pop_tag = True |
|||
try: |
|||
if token.value in _statement_keywords: |
|||
return getattr(self, 'parse_' + self.stream.current.value)() |
|||
if token.value == 'call': |
|||
return self.parse_call_block() |
|||
if token.value == 'filter': |
|||
return self.parse_filter_block() |
|||
ext = self.extensions.get(token.value) |
|||
if ext is not None: |
|||
return ext(self) |
|||
|
|||
# did not work out, remove the token we pushed by accident |
|||
# from the stack so that the unknown tag fail function can |
|||
# produce a proper error message. |
|||
self._tag_stack.pop() |
|||
pop_tag = False |
|||
self.fail_unknown_tag(token.value, token.lineno) |
|||
finally: |
|||
if pop_tag: |
|||
self._tag_stack.pop() |
|||
|
|||
def parse_statements(self, end_tokens, drop_needle=False): |
|||
"""Parse multiple statements into a list until one of the end tokens |
|||
is reached. This is used to parse the body of statements as it also |
|||
parses template data if appropriate. The parser checks first if the |
|||
current token is a colon and skips it if there is one. Then it checks |
|||
for the block end and parses until if one of the `end_tokens` is |
|||
reached. Per default the active token in the stream at the end of |
|||
the call is the matched end token. If this is not wanted `drop_needle` |
|||
can be set to `True` and the end token is removed. |
|||
""" |
|||
# the first token may be a colon for python compatibility |
|||
self.stream.skip_if('colon') |
|||
|
|||
# in the future it would be possible to add whole code sections |
|||
# by adding some sort of end of statement token and parsing those here. |
|||
self.stream.expect('block_end') |
|||
result = self.subparse(end_tokens) |
|||
|
|||
# we reached the end of the template too early, the subparser |
|||
# does not check for this, so we do that now |
|||
if self.stream.current.type == 'eof': |
|||
self.fail_eof(end_tokens) |
|||
|
|||
if drop_needle: |
|||
next(self.stream) |
|||
return result |
|||
|
|||
def parse_set(self): |
|||
"""Parse an assign statement.""" |
|||
lineno = next(self.stream).lineno |
|||
target = self.parse_assign_target() |
|||
self.stream.expect('assign') |
|||
expr = self.parse_tuple() |
|||
return nodes.Assign(target, expr, lineno=lineno) |
|||
|
|||
def parse_for(self): |
|||
"""Parse a for loop.""" |
|||
lineno = self.stream.expect('name:for').lineno |
|||
target = self.parse_assign_target(extra_end_rules=('name:in',)) |
|||
self.stream.expect('name:in') |
|||
iter = self.parse_tuple(with_condexpr=False, |
|||
extra_end_rules=('name:recursive',)) |
|||
test = None |
|||
if self.stream.skip_if('name:if'): |
|||
test = self.parse_expression() |
|||
recursive = self.stream.skip_if('name:recursive') |
|||
body = self.parse_statements(('name:endfor', 'name:else')) |
|||
if next(self.stream).value == 'endfor': |
|||
else_ = [] |
|||
else: |
|||
else_ = self.parse_statements(('name:endfor',), drop_needle=True) |
|||
return nodes.For(target, iter, body, else_, test, |
|||
recursive, lineno=lineno) |
|||
|
|||
def parse_if(self): |
|||
"""Parse an if construct.""" |
|||
node = result = nodes.If(lineno=self.stream.expect('name:if').lineno) |
|||
while 1: |
|||
node.test = self.parse_tuple(with_condexpr=False) |
|||
node.body = self.parse_statements(('name:elif', 'name:else', |
|||
'name:endif')) |
|||
token = next(self.stream) |
|||
if token.test('name:elif'): |
|||
new_node = nodes.If(lineno=self.stream.current.lineno) |
|||
node.else_ = [new_node] |
|||
node = new_node |
|||
continue |
|||
elif token.test('name:else'): |
|||
node.else_ = self.parse_statements(('name:endif',), |
|||
drop_needle=True) |
|||
else: |
|||
node.else_ = [] |
|||
break |
|||
return result |
|||
|
|||
def parse_block(self): |
|||
node = nodes.Block(lineno=next(self.stream).lineno) |
|||
node.name = self.stream.expect('name').value |
|||
node.scoped = self.stream.skip_if('name:scoped') |
|||
|
|||
# common problem people encounter when switching from django |
|||
# to jinja. we do not support hyphens in block names, so let's |
|||
# raise a nicer error message in that case. |
|||
if self.stream.current.type == 'sub': |
|||
self.fail('Block names in Jinja have to be valid Python ' |
|||
'identifiers and may not contain hypens, use an ' |
|||
'underscore instead.') |
|||
|
|||
node.body = self.parse_statements(('name:endblock',), drop_needle=True) |
|||
self.stream.skip_if('name:' + node.name) |
|||
return node |
|||
|
|||
def parse_extends(self): |
|||
node = nodes.Extends(lineno=next(self.stream).lineno) |
|||
node.template = self.parse_expression() |
|||
return node |
|||
|
|||
def parse_import_context(self, node, default): |
|||
if self.stream.current.test_any('name:with', 'name:without') and \ |
|||
self.stream.look().test('name:context'): |
|||
node.with_context = next(self.stream).value == 'with' |
|||
self.stream.skip() |
|||
else: |
|||
node.with_context = default |
|||
return node |
|||
|
|||
def parse_include(self): |
|||
node = nodes.Include(lineno=next(self.stream).lineno) |
|||
node.template = self.parse_expression() |
|||
if self.stream.current.test('name:ignore') and \ |
|||
self.stream.look().test('name:missing'): |
|||
node.ignore_missing = True |
|||
self.stream.skip(2) |
|||
else: |
|||
node.ignore_missing = False |
|||
return self.parse_import_context(node, True) |
|||
|
|||
def parse_import(self): |
|||
node = nodes.Import(lineno=next(self.stream).lineno) |
|||
node.template = self.parse_expression() |
|||
self.stream.expect('name:as') |
|||
node.target = self.parse_assign_target(name_only=True).name |
|||
return self.parse_import_context(node, False) |
|||
|
|||
def parse_from(self): |
|||
node = nodes.FromImport(lineno=next(self.stream).lineno) |
|||
node.template = self.parse_expression() |
|||
self.stream.expect('name:import') |
|||
node.names = [] |
|||
|
|||
def parse_context(): |
|||
if self.stream.current.value in ('with', 'without') and \ |
|||
self.stream.look().test('name:context'): |
|||
node.with_context = next(self.stream).value == 'with' |
|||
self.stream.skip() |
|||
return True |
|||
return False |
|||
|
|||
while 1: |
|||
if node.names: |
|||
self.stream.expect('comma') |
|||
if self.stream.current.type == 'name': |
|||
if parse_context(): |
|||
break |
|||
target = self.parse_assign_target(name_only=True) |
|||
if target.name.startswith('_'): |
|||
self.fail('names starting with an underline can not ' |
|||
'be imported', target.lineno, |
|||
exc=TemplateAssertionError) |
|||
if self.stream.skip_if('name:as'): |
|||
alias = self.parse_assign_target(name_only=True) |
|||
node.names.append((target.name, alias.name)) |
|||
else: |
|||
node.names.append(target.name) |
|||
if parse_context() or self.stream.current.type != 'comma': |
|||
break |
|||
else: |
|||
break |
|||
if not hasattr(node, 'with_context'): |
|||
node.with_context = False |
|||
self.stream.skip_if('comma') |
|||
return node |
|||
|
|||
def parse_signature(self, node): |
|||
node.args = args = [] |
|||
node.defaults = defaults = [] |
|||
self.stream.expect('lparen') |
|||
while self.stream.current.type != 'rparen': |
|||
if args: |
|||
self.stream.expect('comma') |
|||
arg = self.parse_assign_target(name_only=True) |
|||
arg.set_ctx('param') |
|||
if self.stream.skip_if('assign'): |
|||
defaults.append(self.parse_expression()) |
|||
args.append(arg) |
|||
self.stream.expect('rparen') |
|||
|
|||
def parse_call_block(self): |
|||
node = nodes.CallBlock(lineno=next(self.stream).lineno) |
|||
if self.stream.current.type == 'lparen': |
|||
self.parse_signature(node) |
|||
else: |
|||
node.args = [] |
|||
node.defaults = [] |
|||
|
|||
node.call = self.parse_expression() |
|||
if not isinstance(node.call, nodes.Call): |
|||
self.fail('expected call', node.lineno) |
|||
node.body = self.parse_statements(('name:endcall',), drop_needle=True) |
|||
return node |
|||
|
|||
def parse_filter_block(self): |
|||
node = nodes.FilterBlock(lineno=next(self.stream).lineno) |
|||
node.filter = self.parse_filter(None, start_inline=True) |
|||
node.body = self.parse_statements(('name:endfilter',), |
|||
drop_needle=True) |
|||
return node |
|||
|
|||
def parse_macro(self): |
|||
node = nodes.Macro(lineno=next(self.stream).lineno) |
|||
node.name = self.parse_assign_target(name_only=True).name |
|||
self.parse_signature(node) |
|||
node.body = self.parse_statements(('name:endmacro',), |
|||
drop_needle=True) |
|||
return node |
|||
|
|||
def parse_print(self): |
|||
node = nodes.Output(lineno=next(self.stream).lineno) |
|||
node.nodes = [] |
|||
while self.stream.current.type != 'block_end': |
|||
if node.nodes: |
|||
self.stream.expect('comma') |
|||
node.nodes.append(self.parse_expression()) |
|||
return node |
|||
|
|||
def parse_assign_target(self, with_tuple=True, name_only=False, |
|||
extra_end_rules=None): |
|||
"""Parse an assignment target. As Jinja2 allows assignments to |
|||
tuples, this function can parse all allowed assignment targets. Per |
|||
default assignments to tuples are parsed, that can be disable however |
|||
by setting `with_tuple` to `False`. If only assignments to names are |
|||
wanted `name_only` can be set to `True`. The `extra_end_rules` |
|||
parameter is forwarded to the tuple parsing function. |
|||
""" |
|||
if name_only: |
|||
token = self.stream.expect('name') |
|||
target = nodes.Name(token.value, 'store', lineno=token.lineno) |
|||
else: |
|||
if with_tuple: |
|||
target = self.parse_tuple(simplified=True, |
|||
extra_end_rules=extra_end_rules) |
|||
else: |
|||
target = self.parse_primary() |
|||
target.set_ctx('store') |
|||
if not target.can_assign(): |
|||
self.fail('can\'t assign to %r' % target.__class__. |
|||
__name__.lower(), target.lineno) |
|||
return target |
|||
|
|||
def parse_expression(self, with_condexpr=True): |
|||
"""Parse an expression. Per default all expressions are parsed, if |
|||
the optional `with_condexpr` parameter is set to `False` conditional |
|||
expressions are not parsed. |
|||
""" |
|||
if with_condexpr: |
|||
return self.parse_condexpr() |
|||
return self.parse_or() |
|||
|
|||
def parse_condexpr(self): |
|||
lineno = self.stream.current.lineno |
|||
expr1 = self.parse_or() |
|||
while self.stream.skip_if('name:if'): |
|||
expr2 = self.parse_or() |
|||
if self.stream.skip_if('name:else'): |
|||
expr3 = self.parse_condexpr() |
|||
else: |
|||
expr3 = None |
|||
expr1 = nodes.CondExpr(expr2, expr1, expr3, lineno=lineno) |
|||
lineno = self.stream.current.lineno |
|||
return expr1 |
|||
|
|||
def parse_or(self): |
|||
lineno = self.stream.current.lineno |
|||
left = self.parse_and() |
|||
while self.stream.skip_if('name:or'): |
|||
right = self.parse_and() |
|||
left = nodes.Or(left, right, lineno=lineno) |
|||
lineno = self.stream.current.lineno |
|||
return left |
|||
|
|||
def parse_and(self): |
|||
lineno = self.stream.current.lineno |
|||
left = self.parse_not() |
|||
while self.stream.skip_if('name:and'): |
|||
right = self.parse_not() |
|||
left = nodes.And(left, right, lineno=lineno) |
|||
lineno = self.stream.current.lineno |
|||
return left |
|||
|
|||
def parse_not(self): |
|||
if self.stream.current.test('name:not'): |
|||
lineno = next(self.stream).lineno |
|||
return nodes.Not(self.parse_not(), lineno=lineno) |
|||
return self.parse_compare() |
|||
|
|||
def parse_compare(self): |
|||
lineno = self.stream.current.lineno |
|||
expr = self.parse_add() |
|||
ops = [] |
|||
while 1: |
|||
token_type = self.stream.current.type |
|||
if token_type in _compare_operators: |
|||
next(self.stream) |
|||
ops.append(nodes.Operand(token_type, self.parse_add())) |
|||
elif self.stream.skip_if('name:in'): |
|||
ops.append(nodes.Operand('in', self.parse_add())) |
|||
elif self.stream.current.test('name:not') and \ |
|||
self.stream.look().test('name:in'): |
|||
self.stream.skip(2) |
|||
ops.append(nodes.Operand('notin', self.parse_add())) |
|||
else: |
|||
break |
|||
lineno = self.stream.current.lineno |
|||
if not ops: |
|||
return expr |
|||
return nodes.Compare(expr, ops, lineno=lineno) |
|||
|
|||
def parse_add(self): |
|||
lineno = self.stream.current.lineno |
|||
left = self.parse_sub() |
|||
while self.stream.current.type == 'add': |
|||
next(self.stream) |
|||
right = self.parse_sub() |
|||
left = nodes.Add(left, right, lineno=lineno) |
|||
lineno = self.stream.current.lineno |
|||
return left |
|||
|
|||
def parse_sub(self): |
|||
lineno = self.stream.current.lineno |
|||
left = self.parse_concat() |
|||
while self.stream.current.type == 'sub': |
|||
next(self.stream) |
|||
right = self.parse_concat() |
|||
left = nodes.Sub(left, right, lineno=lineno) |
|||
lineno = self.stream.current.lineno |
|||
return left |
|||
|
|||
def parse_concat(self): |
|||
lineno = self.stream.current.lineno |
|||
args = [self.parse_mul()] |
|||
while self.stream.current.type == 'tilde': |
|||
next(self.stream) |
|||
args.append(self.parse_mul()) |
|||
if len(args) == 1: |
|||
return args[0] |
|||
return nodes.Concat(args, lineno=lineno) |
|||
|
|||
def parse_mul(self): |
|||
lineno = self.stream.current.lineno |
|||
left = self.parse_div() |
|||
while self.stream.current.type == 'mul': |
|||
next(self.stream) |
|||
right = self.parse_div() |
|||
left = nodes.Mul(left, right, lineno=lineno) |
|||
lineno = self.stream.current.lineno |
|||
return left |
|||
|
|||
def parse_div(self): |
|||
lineno = self.stream.current.lineno |
|||
left = self.parse_floordiv() |
|||
while self.stream.current.type == 'div': |
|||
next(self.stream) |
|||
right = self.parse_floordiv() |
|||
left = nodes.Div(left, right, lineno=lineno) |
|||
lineno = self.stream.current.lineno |
|||
return left |
|||
|
|||
def parse_floordiv(self): |
|||
lineno = self.stream.current.lineno |
|||
left = self.parse_mod() |
|||
while self.stream.current.type == 'floordiv': |
|||
next(self.stream) |
|||
right = self.parse_mod() |
|||
left = nodes.FloorDiv(left, right, lineno=lineno) |
|||
lineno = self.stream.current.lineno |
|||
return left |
|||
|
|||
def parse_mod(self): |
|||
lineno = self.stream.current.lineno |
|||
left = self.parse_pow() |
|||
while self.stream.current.type == 'mod': |
|||
next(self.stream) |
|||
right = self.parse_pow() |
|||
left = nodes.Mod(left, right, lineno=lineno) |
|||
lineno = self.stream.current.lineno |
|||
return left |
|||
|
|||
def parse_pow(self): |
|||
lineno = self.stream.current.lineno |
|||
left = self.parse_unary() |
|||
while self.stream.current.type == 'pow': |
|||
next(self.stream) |
|||
right = self.parse_unary() |
|||
left = nodes.Pow(left, right, lineno=lineno) |
|||
lineno = self.stream.current.lineno |
|||
return left |
|||
|
|||
def parse_unary(self, with_filter=True): |
|||
token_type = self.stream.current.type |
|||
lineno = self.stream.current.lineno |
|||
if token_type == 'sub': |
|||
next(self.stream) |
|||
node = nodes.Neg(self.parse_unary(False), lineno=lineno) |
|||
elif token_type == 'add': |
|||
next(self.stream) |
|||
node = nodes.Pos(self.parse_unary(False), lineno=lineno) |
|||
else: |
|||
node = self.parse_primary() |
|||
node = self.parse_postfix(node) |
|||
if with_filter: |
|||
node = self.parse_filter_expr(node) |
|||
return node |
|||
|
|||
def parse_primary(self): |
|||
token = self.stream.current |
|||
if token.type == 'name': |
|||
if token.value in ('true', 'false', 'True', 'False'): |
|||
node = nodes.Const(token.value in ('true', 'True'), |
|||
lineno=token.lineno) |
|||
elif token.value in ('none', 'None'): |
|||
node = nodes.Const(None, lineno=token.lineno) |
|||
else: |
|||
node = nodes.Name(token.value, 'load', lineno=token.lineno) |
|||
next(self.stream) |
|||
elif token.type == 'string': |
|||
next(self.stream) |
|||
buf = [token.value] |
|||
lineno = token.lineno |
|||
while self.stream.current.type == 'string': |
|||
buf.append(self.stream.current.value) |
|||
next(self.stream) |
|||
node = nodes.Const(''.join(buf), lineno=lineno) |
|||
elif token.type in ('integer', 'float'): |
|||
next(self.stream) |
|||
node = nodes.Const(token.value, lineno=token.lineno) |
|||
elif token.type == 'lparen': |
|||
next(self.stream) |
|||
node = self.parse_tuple(explicit_parentheses=True) |
|||
self.stream.expect('rparen') |
|||
elif token.type == 'lbracket': |
|||
node = self.parse_list() |
|||
elif token.type == 'lbrace': |
|||
node = self.parse_dict() |
|||
else: |
|||
self.fail("unexpected '%s'" % describe_token(token), token.lineno) |
|||
return node |
|||
|
|||
def parse_tuple(self, simplified=False, with_condexpr=True, |
|||
extra_end_rules=None, explicit_parentheses=False): |
|||
"""Works like `parse_expression` but if multiple expressions are |
|||
delimited by a comma a :class:`~jinja2.nodes.Tuple` node is created. |
|||
This method could also return a regular expression instead of a tuple |
|||
if no commas where found. |
|||
|
|||
The default parsing mode is a full tuple. If `simplified` is `True` |
|||
only names and literals are parsed. The `no_condexpr` parameter is |
|||
forwarded to :meth:`parse_expression`. |
|||
|
|||
Because tuples do not require delimiters and may end in a bogus comma |
|||
an extra hint is needed that marks the end of a tuple. For example |
|||
for loops support tuples between `for` and `in`. In that case the |
|||
`extra_end_rules` is set to ``['name:in']``. |
|||
|
|||
`explicit_parentheses` is true if the parsing was triggered by an |
|||
expression in parentheses. This is used to figure out if an empty |
|||
tuple is a valid expression or not. |
|||
""" |
|||
lineno = self.stream.current.lineno |
|||
if simplified: |
|||
parse = self.parse_primary |
|||
elif with_condexpr: |
|||
parse = self.parse_expression |
|||
else: |
|||
parse = lambda: self.parse_expression(with_condexpr=False) |
|||
args = [] |
|||
is_tuple = False |
|||
while 1: |
|||
if args: |
|||
self.stream.expect('comma') |
|||
if self.is_tuple_end(extra_end_rules): |
|||
break |
|||
args.append(parse()) |
|||
if self.stream.current.type == 'comma': |
|||
is_tuple = True |
|||
else: |
|||
break |
|||
lineno = self.stream.current.lineno |
|||
|
|||
if not is_tuple: |
|||
if args: |
|||
return args[0] |
|||
|
|||
# if we don't have explicit parentheses, an empty tuple is |
|||
# not a valid expression. This would mean nothing (literally |
|||
# nothing) in the spot of an expression would be an empty |
|||
# tuple. |
|||
if not explicit_parentheses: |
|||
self.fail('Expected an expression, got \'%s\'' % |
|||
describe_token(self.stream.current)) |
|||
|
|||
return nodes.Tuple(args, 'load', lineno=lineno) |
|||
|
|||
def parse_list(self): |
|||
token = self.stream.expect('lbracket') |
|||
items = [] |
|||
while self.stream.current.type != 'rbracket': |
|||
if items: |
|||
self.stream.expect('comma') |
|||
if self.stream.current.type == 'rbracket': |
|||
break |
|||
items.append(self.parse_expression()) |
|||
self.stream.expect('rbracket') |
|||
return nodes.List(items, lineno=token.lineno) |
|||
|
|||
def parse_dict(self): |
|||
token = self.stream.expect('lbrace') |
|||
items = [] |
|||
while self.stream.current.type != 'rbrace': |
|||
if items: |
|||
self.stream.expect('comma') |
|||
if self.stream.current.type == 'rbrace': |
|||
break |
|||
key = self.parse_expression() |
|||
self.stream.expect('colon') |
|||
value = self.parse_expression() |
|||
items.append(nodes.Pair(key, value, lineno=key.lineno)) |
|||
self.stream.expect('rbrace') |
|||
return nodes.Dict(items, lineno=token.lineno) |
|||
|
|||
def parse_postfix(self, node): |
|||
while 1: |
|||
token_type = self.stream.current.type |
|||
if token_type == 'dot' or token_type == 'lbracket': |
|||
node = self.parse_subscript(node) |
|||
# calls are valid both after postfix expressions (getattr |
|||
# and getitem) as well as filters and tests |
|||
elif token_type == 'lparen': |
|||
node = self.parse_call(node) |
|||
else: |
|||
break |
|||
return node |
|||
|
|||
def parse_filter_expr(self, node): |
|||
while 1: |
|||
token_type = self.stream.current.type |
|||
if token_type == 'pipe': |
|||
node = self.parse_filter(node) |
|||
elif token_type == 'name' and self.stream.current.value == 'is': |
|||
node = self.parse_test(node) |
|||
# calls are valid both after postfix expressions (getattr |
|||
# and getitem) as well as filters and tests |
|||
elif token_type == 'lparen': |
|||
node = self.parse_call(node) |
|||
else: |
|||
break |
|||
return node |
|||
|
|||
def parse_subscript(self, node): |
|||
token = next(self.stream) |
|||
if token.type == 'dot': |
|||
attr_token = self.stream.current |
|||
next(self.stream) |
|||
if attr_token.type == 'name': |
|||
return nodes.Getattr(node, attr_token.value, 'load', |
|||
lineno=token.lineno) |
|||
elif attr_token.type != 'integer': |
|||
self.fail('expected name or number', attr_token.lineno) |
|||
arg = nodes.Const(attr_token.value, lineno=attr_token.lineno) |
|||
return nodes.Getitem(node, arg, 'load', lineno=token.lineno) |
|||
if token.type == 'lbracket': |
|||
priority_on_attribute = False |
|||
args = [] |
|||
while self.stream.current.type != 'rbracket': |
|||
if args: |
|||
self.stream.expect('comma') |
|||
args.append(self.parse_subscribed()) |
|||
self.stream.expect('rbracket') |
|||
if len(args) == 1: |
|||
arg = args[0] |
|||
else: |
|||
arg = nodes.Tuple(args, 'load', lineno=token.lineno) |
|||
return nodes.Getitem(node, arg, 'load', lineno=token.lineno) |
|||
self.fail('expected subscript expression', self.lineno) |
|||
|
|||
def parse_subscribed(self): |
|||
lineno = self.stream.current.lineno |
|||
|
|||
if self.stream.current.type == 'colon': |
|||
next(self.stream) |
|||
args = [None] |
|||
else: |
|||
node = self.parse_expression() |
|||
if self.stream.current.type != 'colon': |
|||
return node |
|||
next(self.stream) |
|||
args = [node] |
|||
|
|||
if self.stream.current.type == 'colon': |
|||
args.append(None) |
|||
elif self.stream.current.type not in ('rbracket', 'comma'): |
|||
args.append(self.parse_expression()) |
|||
else: |
|||
args.append(None) |
|||
|
|||
if self.stream.current.type == 'colon': |
|||
next(self.stream) |
|||
if self.stream.current.type not in ('rbracket', 'comma'): |
|||
args.append(self.parse_expression()) |
|||
else: |
|||
args.append(None) |
|||
else: |
|||
args.append(None) |
|||
|
|||
return nodes.Slice(lineno=lineno, *args) |
|||
|
|||
def parse_call(self, node): |
|||
token = self.stream.expect('lparen') |
|||
args = [] |
|||
kwargs = [] |
|||
dyn_args = dyn_kwargs = None |
|||
require_comma = False |
|||
|
|||
def ensure(expr): |
|||
if not expr: |
|||
self.fail('invalid syntax for function call expression', |
|||
token.lineno) |
|||
|
|||
while self.stream.current.type != 'rparen': |
|||
if require_comma: |
|||
self.stream.expect('comma') |
|||
# support for trailing comma |
|||
if self.stream.current.type == 'rparen': |
|||
break |
|||
if self.stream.current.type == 'mul': |
|||
ensure(dyn_args is None and dyn_kwargs is None) |
|||
next(self.stream) |
|||
dyn_args = self.parse_expression() |
|||
elif self.stream.current.type == 'pow': |
|||
ensure(dyn_kwargs is None) |
|||
next(self.stream) |
|||
dyn_kwargs = self.parse_expression() |
|||
else: |
|||
ensure(dyn_args is None and dyn_kwargs is None) |
|||
if self.stream.current.type == 'name' and \ |
|||
self.stream.look().type == 'assign': |
|||
key = self.stream.current.value |
|||
self.stream.skip(2) |
|||
value = self.parse_expression() |
|||
kwargs.append(nodes.Keyword(key, value, |
|||
lineno=value.lineno)) |
|||
else: |
|||
ensure(not kwargs) |
|||
args.append(self.parse_expression()) |
|||
|
|||
require_comma = True |
|||
self.stream.expect('rparen') |
|||
|
|||
if node is None: |
|||
return args, kwargs, dyn_args, dyn_kwargs |
|||
return nodes.Call(node, args, kwargs, dyn_args, dyn_kwargs, |
|||
lineno=token.lineno) |
|||
|
|||
def parse_filter(self, node, start_inline=False): |
|||
while self.stream.current.type == 'pipe' or start_inline: |
|||
if not start_inline: |
|||
next(self.stream) |
|||
token = self.stream.expect('name') |
|||
name = token.value |
|||
while self.stream.current.type == 'dot': |
|||
next(self.stream) |
|||
name += '.' + self.stream.expect('name').value |
|||
if self.stream.current.type == 'lparen': |
|||
args, kwargs, dyn_args, dyn_kwargs = self.parse_call(None) |
|||
else: |
|||
args = [] |
|||
kwargs = [] |
|||
dyn_args = dyn_kwargs = None |
|||
node = nodes.Filter(node, name, args, kwargs, dyn_args, |
|||
dyn_kwargs, lineno=token.lineno) |
|||
start_inline = False |
|||
return node |
|||
|
|||
def parse_test(self, node): |
|||
token = next(self.stream) |
|||
if self.stream.current.test('name:not'): |
|||
next(self.stream) |
|||
negated = True |
|||
else: |
|||
negated = False |
|||
name = self.stream.expect('name').value |
|||
while self.stream.current.type == 'dot': |
|||
next(self.stream) |
|||
name += '.' + self.stream.expect('name').value |
|||
dyn_args = dyn_kwargs = None |
|||
kwargs = [] |
|||
if self.stream.current.type == 'lparen': |
|||
args, kwargs, dyn_args, dyn_kwargs = self.parse_call(None) |
|||
elif self.stream.current.type in ('name', 'string', 'integer', |
|||
'float', 'lparen', 'lbracket', |
|||
'lbrace') and not \ |
|||
self.stream.current.test_any('name:else', 'name:or', |
|||
'name:and'): |
|||
if self.stream.current.test('name:is'): |
|||
self.fail('You cannot chain multiple tests with is') |
|||
args = [self.parse_expression()] |
|||
else: |
|||
args = [] |
|||
node = nodes.Test(node, name, args, kwargs, dyn_args, |
|||
dyn_kwargs, lineno=token.lineno) |
|||
if negated: |
|||
node = nodes.Not(node, lineno=token.lineno) |
|||
return node |
|||
|
|||
def subparse(self, end_tokens=None): |
|||
body = [] |
|||
data_buffer = [] |
|||
add_data = data_buffer.append |
|||
|
|||
if end_tokens is not None: |
|||
self._end_token_stack.append(end_tokens) |
|||
|
|||
def flush_data(): |
|||
if data_buffer: |
|||
lineno = data_buffer[0].lineno |
|||
body.append(nodes.Output(data_buffer[:], lineno=lineno)) |
|||
del data_buffer[:] |
|||
|
|||
try: |
|||
while self.stream: |
|||
token = self.stream.current |
|||
if token.type == 'data': |
|||
if token.value: |
|||
add_data(nodes.TemplateData(token.value, |
|||
lineno=token.lineno)) |
|||
next(self.stream) |
|||
elif token.type == 'variable_begin': |
|||
next(self.stream) |
|||
add_data(self.parse_tuple(with_condexpr=True)) |
|||
self.stream.expect('variable_end') |
|||
elif token.type == 'block_begin': |
|||
flush_data() |
|||
next(self.stream) |
|||
if end_tokens is not None and \ |
|||
self.stream.current.test_any(*end_tokens): |
|||
return body |
|||
rv = self.parse_statement() |
|||
if isinstance(rv, list): |
|||
body.extend(rv) |
|||
else: |
|||
body.append(rv) |
|||
self.stream.expect('block_end') |
|||
else: |
|||
raise AssertionError('internal parsing error') |
|||
|
|||
flush_data() |
|||
finally: |
|||
if end_tokens is not None: |
|||
self._end_token_stack.pop() |
|||
|
|||
return body |
|||
|
|||
def parse(self): |
|||
"""Parse the whole template into a `Template` node.""" |
|||
result = nodes.Template(self.subparse(), lineno=1) |
|||
result.set_environment(self.environment) |
|||
return result |
@ -1,548 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
jinja2.runtime |
|||
~~~~~~~~~~~~~~ |
|||
|
|||
Runtime helpers. |
|||
|
|||
:copyright: (c) 2010 by the Jinja Team. |
|||
:license: BSD. |
|||
""" |
|||
from itertools import chain, imap |
|||
from jinja2.nodes import EvalContext, _context_function_types |
|||
from jinja2.utils import Markup, partial, soft_unicode, escape, missing, \ |
|||
concat, internalcode, next, object_type_repr |
|||
from jinja2.exceptions import UndefinedError, TemplateRuntimeError, \ |
|||
TemplateNotFound |
|||
|
|||
|
|||
# these variables are exported to the template runtime |
|||
__all__ = ['LoopContext', 'TemplateReference', 'Macro', 'Markup', |
|||
'TemplateRuntimeError', 'missing', 'concat', 'escape', |
|||
'markup_join', 'unicode_join', 'to_string', 'identity', |
|||
'TemplateNotFound'] |
|||
|
|||
#: the name of the function that is used to convert something into |
|||
#: a string. 2to3 will adopt that automatically and the generated |
|||
#: code can take advantage of it. |
|||
to_string = unicode |
|||
|
|||
#: the identity function. Useful for certain things in the environment |
|||
identity = lambda x: x |
|||
|
|||
|
|||
def markup_join(seq): |
|||
"""Concatenation that escapes if necessary and converts to unicode.""" |
|||
buf = [] |
|||
iterator = imap(soft_unicode, seq) |
|||
for arg in iterator: |
|||
buf.append(arg) |
|||
if hasattr(arg, '__html__'): |
|||
return Markup(u'').join(chain(buf, iterator)) |
|||
return concat(buf) |
|||
|
|||
|
|||
def unicode_join(seq): |
|||
"""Simple args to unicode conversion and concatenation.""" |
|||
return concat(imap(unicode, seq)) |
|||
|
|||
|
|||
def new_context(environment, template_name, blocks, vars=None, |
|||
shared=None, globals=None, locals=None): |
|||
"""Internal helper to for context creation.""" |
|||
if vars is None: |
|||
vars = {} |
|||
if shared: |
|||
parent = vars |
|||
else: |
|||
parent = dict(globals or (), **vars) |
|||
if locals: |
|||
# if the parent is shared a copy should be created because |
|||
# we don't want to modify the dict passed |
|||
if shared: |
|||
parent = dict(parent) |
|||
for key, value in locals.iteritems(): |
|||
if key[:2] == 'l_' and value is not missing: |
|||
parent[key[2:]] = value |
|||
return Context(environment, parent, template_name, blocks) |
|||
|
|||
|
|||
class TemplateReference(object): |
|||
"""The `self` in templates.""" |
|||
|
|||
def __init__(self, context): |
|||
self.__context = context |
|||
|
|||
def __getitem__(self, name): |
|||
blocks = self.__context.blocks[name] |
|||
return BlockReference(name, self.__context, blocks, 0) |
|||
|
|||
def __repr__(self): |
|||
return '<%s %r>' % ( |
|||
self.__class__.__name__, |
|||
self.__context.name |
|||
) |
|||
|
|||
|
|||
class Context(object): |
|||
"""The template context holds the variables of a template. It stores the |
|||
values passed to the template and also the names the template exports. |
|||
Creating instances is neither supported nor useful as it's created |
|||
automatically at various stages of the template evaluation and should not |
|||
be created by hand. |
|||
|
|||
The context is immutable. Modifications on :attr:`parent` **must not** |
|||
happen and modifications on :attr:`vars` are allowed from generated |
|||
template code only. Template filters and global functions marked as |
|||
:func:`contextfunction`\s get the active context passed as first argument |
|||
and are allowed to access the context read-only. |
|||
|
|||
The template context supports read only dict operations (`get`, |
|||
`keys`, `values`, `items`, `iterkeys`, `itervalues`, `iteritems`, |
|||
`__getitem__`, `__contains__`). Additionally there is a :meth:`resolve` |
|||
method that doesn't fail with a `KeyError` but returns an |
|||
:class:`Undefined` object for missing variables. |
|||
""" |
|||
__slots__ = ('parent', 'vars', 'environment', 'eval_ctx', 'exported_vars', |
|||
'name', 'blocks', '__weakref__') |
|||
|
|||
def __init__(self, environment, parent, name, blocks): |
|||
self.parent = parent |
|||
self.vars = {} |
|||
self.environment = environment |
|||
self.eval_ctx = EvalContext(self.environment, name) |
|||
self.exported_vars = set() |
|||
self.name = name |
|||
|
|||
# create the initial mapping of blocks. Whenever template inheritance |
|||
# takes place the runtime will update this mapping with the new blocks |
|||
# from the template. |
|||
self.blocks = dict((k, [v]) for k, v in blocks.iteritems()) |
|||
|
|||
def super(self, name, current): |
|||
"""Render a parent block.""" |
|||
try: |
|||
blocks = self.blocks[name] |
|||
index = blocks.index(current) + 1 |
|||
blocks[index] |
|||
except LookupError: |
|||
return self.environment.undefined('there is no parent block ' |
|||
'called %r.' % name, |
|||
name='super') |
|||
return BlockReference(name, self, blocks, index) |
|||
|
|||
def get(self, key, default=None): |
|||
"""Returns an item from the template context, if it doesn't exist |
|||
`default` is returned. |
|||
""" |
|||
try: |
|||
return self[key] |
|||
except KeyError: |
|||
return default |
|||
|
|||
def resolve(self, key): |
|||
"""Looks up a variable like `__getitem__` or `get` but returns an |
|||
:class:`Undefined` object with the name of the name looked up. |
|||
""" |
|||
if key in self.vars: |
|||
return self.vars[key] |
|||
if key in self.parent: |
|||
return self.parent[key] |
|||
return self.environment.undefined(name=key) |
|||
|
|||
def get_exported(self): |
|||
"""Get a new dict with the exported variables.""" |
|||
return dict((k, self.vars[k]) for k in self.exported_vars) |
|||
|
|||
def get_all(self): |
|||
"""Return a copy of the complete context as dict including the |
|||
exported variables. |
|||
""" |
|||
return dict(self.parent, **self.vars) |
|||
|
|||
@internalcode |
|||
def call(__self, __obj, *args, **kwargs): |
|||
"""Call the callable with the arguments and keyword arguments |
|||
provided but inject the active context or environment as first |
|||
argument if the callable is a :func:`contextfunction` or |
|||
:func:`environmentfunction`. |
|||
""" |
|||
if __debug__: |
|||
__traceback_hide__ = True |
|||
if isinstance(__obj, _context_function_types): |
|||
if getattr(__obj, 'contextfunction', 0): |
|||
args = (__self,) + args |
|||
elif getattr(__obj, 'evalcontextfunction', 0): |
|||
args = (__self.eval_ctx,) + args |
|||
elif getattr(__obj, 'environmentfunction', 0): |
|||
args = (__self.environment,) + args |
|||
try: |
|||
return __obj(*args, **kwargs) |
|||
except StopIteration: |
|||
return __self.environment.undefined('value was undefined because ' |
|||
'a callable raised a ' |
|||
'StopIteration exception') |
|||
|
|||
def derived(self, locals=None): |
|||
"""Internal helper function to create a derived context.""" |
|||
context = new_context(self.environment, self.name, {}, |
|||
self.parent, True, None, locals) |
|||
context.vars.update(self.vars) |
|||
context.eval_ctx = self.eval_ctx |
|||
context.blocks.update((k, list(v)) for k, v in self.blocks.iteritems()) |
|||
return context |
|||
|
|||
def _all(meth): |
|||
proxy = lambda self: getattr(self.get_all(), meth)() |
|||
proxy.__doc__ = getattr(dict, meth).__doc__ |
|||
proxy.__name__ = meth |
|||
return proxy |
|||
|
|||
keys = _all('keys') |
|||
values = _all('values') |
|||
items = _all('items') |
|||
|
|||
# not available on python 3 |
|||
if hasattr(dict, 'iterkeys'): |
|||
iterkeys = _all('iterkeys') |
|||
itervalues = _all('itervalues') |
|||
iteritems = _all('iteritems') |
|||
del _all |
|||
|
|||
def __contains__(self, name): |
|||
return name in self.vars or name in self.parent |
|||
|
|||
def __getitem__(self, key): |
|||
"""Lookup a variable or raise `KeyError` if the variable is |
|||
undefined. |
|||
""" |
|||
item = self.resolve(key) |
|||
if isinstance(item, Undefined): |
|||
raise KeyError(key) |
|||
return item |
|||
|
|||
def __repr__(self): |
|||
return '<%s %s of %r>' % ( |
|||
self.__class__.__name__, |
|||
repr(self.get_all()), |
|||
self.name |
|||
) |
|||
|
|||
|
|||
# register the context as mapping if possible |
|||
try: |
|||
from collections import Mapping |
|||
Mapping.register(Context) |
|||
except ImportError: |
|||
pass |
|||
|
|||
|
|||
class BlockReference(object): |
|||
"""One block on a template reference.""" |
|||
|
|||
def __init__(self, name, context, stack, depth): |
|||
self.name = name |
|||
self._context = context |
|||
self._stack = stack |
|||
self._depth = depth |
|||
|
|||
@property |
|||
def super(self): |
|||
"""Super the block.""" |
|||
if self._depth + 1 >= len(self._stack): |
|||
return self._context.environment. \ |
|||
undefined('there is no parent block called %r.' % |
|||
self.name, name='super') |
|||
return BlockReference(self.name, self._context, self._stack, |
|||
self._depth + 1) |
|||
|
|||
@internalcode |
|||
def __call__(self): |
|||
rv = concat(self._stack[self._depth](self._context)) |
|||
if self._context.eval_ctx.autoescape: |
|||
rv = Markup(rv) |
|||
return rv |
|||
|
|||
|
|||
class LoopContext(object): |
|||
"""A loop context for dynamic iteration.""" |
|||
|
|||
def __init__(self, iterable, recurse=None): |
|||
self._iterator = iter(iterable) |
|||
self._recurse = recurse |
|||
self.index0 = -1 |
|||
|
|||
# try to get the length of the iterable early. This must be done |
|||
# here because there are some broken iterators around where there |
|||
# __len__ is the number of iterations left (i'm looking at your |
|||
# listreverseiterator!). |
|||
try: |
|||
self._length = len(iterable) |
|||
except (TypeError, AttributeError): |
|||
self._length = None |
|||
|
|||
def cycle(self, *args): |
|||
"""Cycles among the arguments with the current loop index.""" |
|||
if not args: |
|||
raise TypeError('no items for cycling given') |
|||
return args[self.index0 % len(args)] |
|||
|
|||
first = property(lambda x: x.index0 == 0) |
|||
last = property(lambda x: x.index0 + 1 == x.length) |
|||
index = property(lambda x: x.index0 + 1) |
|||
revindex = property(lambda x: x.length - x.index0) |
|||
revindex0 = property(lambda x: x.length - x.index) |
|||
|
|||
def __len__(self): |
|||
return self.length |
|||
|
|||
def __iter__(self): |
|||
return LoopContextIterator(self) |
|||
|
|||
@internalcode |
|||
def loop(self, iterable): |
|||
if self._recurse is None: |
|||
raise TypeError('Tried to call non recursive loop. Maybe you ' |
|||
"forgot the 'recursive' modifier.") |
|||
return self._recurse(iterable, self._recurse) |
|||
|
|||
# a nifty trick to enhance the error message if someone tried to call |
|||
# the the loop without or with too many arguments. |
|||
__call__ = loop |
|||
del loop |
|||
|
|||
@property |
|||
def length(self): |
|||
if self._length is None: |
|||
# if was not possible to get the length of the iterator when |
|||
# the loop context was created (ie: iterating over a generator) |
|||
# we have to convert the iterable into a sequence and use the |
|||
# length of that. |
|||
iterable = tuple(self._iterator) |
|||
self._iterator = iter(iterable) |
|||
self._length = len(iterable) + self.index0 + 1 |
|||
return self._length |
|||
|
|||
def __repr__(self): |
|||
return '<%s %r/%r>' % ( |
|||
self.__class__.__name__, |
|||
self.index, |
|||
self.length |
|||
) |
|||
|
|||
|
|||
class LoopContextIterator(object): |
|||
"""The iterator for a loop context.""" |
|||
__slots__ = ('context',) |
|||
|
|||
def __init__(self, context): |
|||
self.context = context |
|||
|
|||
def __iter__(self): |
|||
return self |
|||
|
|||
def next(self): |
|||
ctx = self.context |
|||
ctx.index0 += 1 |
|||
return next(ctx._iterator), ctx |
|||
|
|||
|
|||
class Macro(object): |
|||
"""Wraps a macro function.""" |
|||
|
|||
def __init__(self, environment, func, name, arguments, defaults, |
|||
catch_kwargs, catch_varargs, caller): |
|||
self._environment = environment |
|||
self._func = func |
|||
self._argument_count = len(arguments) |
|||
self.name = name |
|||
self.arguments = arguments |
|||
self.defaults = defaults |
|||
self.catch_kwargs = catch_kwargs |
|||
self.catch_varargs = catch_varargs |
|||
self.caller = caller |
|||
|
|||
@internalcode |
|||
def __call__(self, *args, **kwargs): |
|||
# try to consume the positional arguments |
|||
arguments = list(args[:self._argument_count]) |
|||
off = len(arguments) |
|||
|
|||
# if the number of arguments consumed is not the number of |
|||
# arguments expected we start filling in keyword arguments |
|||
# and defaults. |
|||
if off != self._argument_count: |
|||
for idx, name in enumerate(self.arguments[len(arguments):]): |
|||
try: |
|||
value = kwargs.pop(name) |
|||
except KeyError: |
|||
try: |
|||
value = self.defaults[idx - self._argument_count + off] |
|||
except IndexError: |
|||
value = self._environment.undefined( |
|||
'parameter %r was not provided' % name, name=name) |
|||
arguments.append(value) |
|||
|
|||
# it's important that the order of these arguments does not change |
|||
# if not also changed in the compiler's `function_scoping` method. |
|||
# the order is caller, keyword arguments, positional arguments! |
|||
if self.caller: |
|||
caller = kwargs.pop('caller', None) |
|||
if caller is None: |
|||
caller = self._environment.undefined('No caller defined', |
|||
name='caller') |
|||
arguments.append(caller) |
|||
if self.catch_kwargs: |
|||
arguments.append(kwargs) |
|||
elif kwargs: |
|||
raise TypeError('macro %r takes no keyword argument %r' % |
|||
(self.name, next(iter(kwargs)))) |
|||
if self.catch_varargs: |
|||
arguments.append(args[self._argument_count:]) |
|||
elif len(args) > self._argument_count: |
|||
raise TypeError('macro %r takes not more than %d argument(s)' % |
|||
(self.name, len(self.arguments))) |
|||
return self._func(*arguments) |
|||
|
|||
def __repr__(self): |
|||
return '<%s %s>' % ( |
|||
self.__class__.__name__, |
|||
self.name is None and 'anonymous' or repr(self.name) |
|||
) |
|||
|
|||
|
|||
class Undefined(object): |
|||
"""The default undefined type. This undefined type can be printed and |
|||
iterated over, but every other access will raise an :exc:`UndefinedError`: |
|||
|
|||
>>> foo = Undefined(name='foo') |
|||
>>> str(foo) |
|||
'' |
|||
>>> not foo |
|||
True |
|||
>>> foo + 42 |
|||
Traceback (most recent call last): |
|||
... |
|||
UndefinedError: 'foo' is undefined |
|||
""" |
|||
__slots__ = ('_undefined_hint', '_undefined_obj', '_undefined_name', |
|||
'_undefined_exception') |
|||
|
|||
def __init__(self, hint=None, obj=missing, name=None, exc=UndefinedError): |
|||
self._undefined_hint = hint |
|||
self._undefined_obj = obj |
|||
self._undefined_name = name |
|||
self._undefined_exception = exc |
|||
|
|||
@internalcode |
|||
def _fail_with_undefined_error(self, *args, **kwargs): |
|||
"""Regular callback function for undefined objects that raises an |
|||
`UndefinedError` on call. |
|||
""" |
|||
if self._undefined_hint is None: |
|||
if self._undefined_obj is missing: |
|||
hint = '%r is undefined' % self._undefined_name |
|||
elif not isinstance(self._undefined_name, basestring): |
|||
hint = '%s has no element %r' % ( |
|||
object_type_repr(self._undefined_obj), |
|||
self._undefined_name |
|||
) |
|||
else: |
|||
hint = '%r has no attribute %r' % ( |
|||
object_type_repr(self._undefined_obj), |
|||
self._undefined_name |
|||
) |
|||
else: |
|||
hint = self._undefined_hint |
|||
raise self._undefined_exception(hint) |
|||
|
|||
@internalcode |
|||
def __getattr__(self, name): |
|||
if name[:2] == '__': |
|||
raise AttributeError(name) |
|||
return self._fail_with_undefined_error() |
|||
|
|||
__add__ = __radd__ = __mul__ = __rmul__ = __div__ = __rdiv__ = \ |
|||
__truediv__ = __rtruediv__ = __floordiv__ = __rfloordiv__ = \ |
|||
__mod__ = __rmod__ = __pos__ = __neg__ = __call__ = \ |
|||
__getitem__ = __lt__ = __le__ = __gt__ = __ge__ = __int__ = \ |
|||
__float__ = __complex__ = __pow__ = __rpow__ = \ |
|||
_fail_with_undefined_error |
|||
|
|||
def __str__(self): |
|||
return unicode(self).encode('utf-8') |
|||
|
|||
# unicode goes after __str__ because we configured 2to3 to rename |
|||
# __unicode__ to __str__. because the 2to3 tree is not designed to |
|||
# remove nodes from it, we leave the above __str__ around and let |
|||
# it override at runtime. |
|||
def __unicode__(self): |
|||
return u'' |
|||
|
|||
def __len__(self): |
|||
return 0 |
|||
|
|||
def __iter__(self): |
|||
if 0: |
|||
yield None |
|||
|
|||
def __nonzero__(self): |
|||
return False |
|||
|
|||
def __repr__(self): |
|||
return 'Undefined' |
|||
|
|||
|
|||
class DebugUndefined(Undefined): |
|||
"""An undefined that returns the debug info when printed. |
|||
|
|||
>>> foo = DebugUndefined(name='foo') |
|||
>>> str(foo) |
|||
'{{ foo }}' |
|||
>>> not foo |
|||
True |
|||
>>> foo + 42 |
|||
Traceback (most recent call last): |
|||
... |
|||
UndefinedError: 'foo' is undefined |
|||
""" |
|||
__slots__ = () |
|||
|
|||
def __unicode__(self): |
|||
if self._undefined_hint is None: |
|||
if self._undefined_obj is missing: |
|||
return u'{{ %s }}' % self._undefined_name |
|||
return '{{ no such element: %s[%r] }}' % ( |
|||
object_type_repr(self._undefined_obj), |
|||
self._undefined_name |
|||
) |
|||
return u'{{ undefined value printed: %s }}' % self._undefined_hint |
|||
|
|||
|
|||
class StrictUndefined(Undefined): |
|||
"""An undefined that barks on print and iteration as well as boolean |
|||
tests and all kinds of comparisons. In other words: you can do nothing |
|||
with it except checking if it's defined using the `defined` test. |
|||
|
|||
>>> foo = StrictUndefined(name='foo') |
|||
>>> str(foo) |
|||
Traceback (most recent call last): |
|||
... |
|||
UndefinedError: 'foo' is undefined |
|||
>>> not foo |
|||
Traceback (most recent call last): |
|||
... |
|||
UndefinedError: 'foo' is undefined |
|||
>>> foo + 42 |
|||
Traceback (most recent call last): |
|||
... |
|||
UndefinedError: 'foo' is undefined |
|||
""" |
|||
__slots__ = () |
|||
__iter__ = __unicode__ = __str__ = __len__ = __nonzero__ = __eq__ = \ |
|||
__ne__ = __bool__ = Undefined._fail_with_undefined_error |
|||
|
|||
|
|||
# remove remaining slots attributes, after the metaclass did the magic they |
|||
# are unneeded and irritating as they contain wrong data for the subclasses. |
|||
del Undefined.__slots__, DebugUndefined.__slots__, StrictUndefined.__slots__ |
@ -1,361 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
jinja2.sandbox |
|||
~~~~~~~~~~~~~~ |
|||
|
|||
Adds a sandbox layer to Jinja as it was the default behavior in the old |
|||
Jinja 1 releases. This sandbox is slightly different from Jinja 1 as the |
|||
default behavior is easier to use. |
|||
|
|||
The behavior can be changed by subclassing the environment. |
|||
|
|||
:copyright: (c) 2010 by the Jinja Team. |
|||
:license: BSD. |
|||
""" |
|||
import operator |
|||
from jinja2.environment import Environment |
|||
from jinja2.exceptions import SecurityError |
|||
from jinja2.utils import FunctionType, MethodType, TracebackType, CodeType, \ |
|||
FrameType, GeneratorType |
|||
|
|||
|
|||
#: maximum number of items a range may produce |
|||
MAX_RANGE = 100000 |
|||
|
|||
#: attributes of function objects that are considered unsafe. |
|||
UNSAFE_FUNCTION_ATTRIBUTES = set(['func_closure', 'func_code', 'func_dict', |
|||
'func_defaults', 'func_globals']) |
|||
|
|||
#: unsafe method attributes. function attributes are unsafe for methods too |
|||
UNSAFE_METHOD_ATTRIBUTES = set(['im_class', 'im_func', 'im_self']) |
|||
|
|||
|
|||
import warnings |
|||
|
|||
# make sure we don't warn in python 2.6 about stuff we don't care about |
|||
warnings.filterwarnings('ignore', 'the sets module', DeprecationWarning, |
|||
module='jinja2.sandbox') |
|||
|
|||
from collections import deque |
|||
|
|||
_mutable_set_types = (set,) |
|||
_mutable_mapping_types = (dict,) |
|||
_mutable_sequence_types = (list,) |
|||
|
|||
|
|||
# on python 2.x we can register the user collection types |
|||
try: |
|||
from UserDict import UserDict, DictMixin |
|||
from UserList import UserList |
|||
_mutable_mapping_types += (UserDict, DictMixin) |
|||
_mutable_set_types += (UserList,) |
|||
except ImportError: |
|||
pass |
|||
|
|||
# if sets is still available, register the mutable set from there as well |
|||
try: |
|||
from sets import Set |
|||
_mutable_set_types += (Set,) |
|||
except ImportError: |
|||
pass |
|||
|
|||
#: register Python 2.6 abstract base classes |
|||
try: |
|||
from collections import MutableSet, MutableMapping, MutableSequence |
|||
_mutable_set_types += (MutableSet,) |
|||
_mutable_mapping_types += (MutableMapping,) |
|||
_mutable_sequence_types += (MutableSequence,) |
|||
except ImportError: |
|||
pass |
|||
|
|||
_mutable_spec = ( |
|||
(_mutable_set_types, frozenset([ |
|||
'add', 'clear', 'difference_update', 'discard', 'pop', 'remove', |
|||
'symmetric_difference_update', 'update' |
|||
])), |
|||
(_mutable_mapping_types, frozenset([ |
|||
'clear', 'pop', 'popitem', 'setdefault', 'update' |
|||
])), |
|||
(_mutable_sequence_types, frozenset([ |
|||
'append', 'reverse', 'insert', 'sort', 'extend', 'remove' |
|||
])), |
|||
(deque, frozenset([ |
|||
'append', 'appendleft', 'clear', 'extend', 'extendleft', 'pop', |
|||
'popleft', 'remove', 'rotate' |
|||
])) |
|||
) |
|||
|
|||
|
|||
def safe_range(*args): |
|||
"""A range that can't generate ranges with a length of more than |
|||
MAX_RANGE items. |
|||
""" |
|||
rng = xrange(*args) |
|||
if len(rng) > MAX_RANGE: |
|||
raise OverflowError('range too big, maximum size for range is %d' % |
|||
MAX_RANGE) |
|||
return rng |
|||
|
|||
|
|||
def unsafe(f): |
|||
"""Marks a function or method as unsafe. |
|||
|
|||
:: |
|||
|
|||
@unsafe |
|||
def delete(self): |
|||
pass |
|||
""" |
|||
f.unsafe_callable = True |
|||
return f |
|||
|
|||
|
|||
def is_internal_attribute(obj, attr): |
|||
"""Test if the attribute given is an internal python attribute. For |
|||
example this function returns `True` for the `func_code` attribute of |
|||
python objects. This is useful if the environment method |
|||
:meth:`~SandboxedEnvironment.is_safe_attribute` is overriden. |
|||
|
|||
>>> from jinja2.sandbox import is_internal_attribute |
|||
>>> is_internal_attribute(lambda: None, "func_code") |
|||
True |
|||
>>> is_internal_attribute((lambda x:x).func_code, 'co_code') |
|||
True |
|||
>>> is_internal_attribute(str, "upper") |
|||
False |
|||
""" |
|||
if isinstance(obj, FunctionType): |
|||
if attr in UNSAFE_FUNCTION_ATTRIBUTES: |
|||
return True |
|||
elif isinstance(obj, MethodType): |
|||
if attr in UNSAFE_FUNCTION_ATTRIBUTES or \ |
|||
attr in UNSAFE_METHOD_ATTRIBUTES: |
|||
return True |
|||
elif isinstance(obj, type): |
|||
if attr == 'mro': |
|||
return True |
|||
elif isinstance(obj, (CodeType, TracebackType, FrameType)): |
|||
return True |
|||
elif isinstance(obj, GeneratorType): |
|||
if attr == 'gi_frame': |
|||
return True |
|||
return attr.startswith('__') |
|||
|
|||
|
|||
def modifies_known_mutable(obj, attr): |
|||
"""This function checks if an attribute on a builtin mutable object |
|||
(list, dict, set or deque) would modify it if called. It also supports |
|||
the "user"-versions of the objects (`sets.Set`, `UserDict.*` etc.) and |
|||
with Python 2.6 onwards the abstract base classes `MutableSet`, |
|||
`MutableMapping`, and `MutableSequence`. |
|||
|
|||
>>> modifies_known_mutable({}, "clear") |
|||
True |
|||
>>> modifies_known_mutable({}, "keys") |
|||
False |
|||
>>> modifies_known_mutable([], "append") |
|||
True |
|||
>>> modifies_known_mutable([], "index") |
|||
False |
|||
|
|||
If called with an unsupported object (such as unicode) `False` is |
|||
returned. |
|||
|
|||
>>> modifies_known_mutable("foo", "upper") |
|||
False |
|||
""" |
|||
for typespec, unsafe in _mutable_spec: |
|||
if isinstance(obj, typespec): |
|||
return attr in unsafe |
|||
return False |
|||
|
|||
|
|||
class SandboxedEnvironment(Environment): |
|||
"""The sandboxed environment. It works like the regular environment but |
|||
tells the compiler to generate sandboxed code. Additionally subclasses of |
|||
this environment may override the methods that tell the runtime what |
|||
attributes or functions are safe to access. |
|||
|
|||
If the template tries to access insecure code a :exc:`SecurityError` is |
|||
raised. However also other exceptions may occour during the rendering so |
|||
the caller has to ensure that all exceptions are catched. |
|||
""" |
|||
sandboxed = True |
|||
|
|||
#: default callback table for the binary operators. A copy of this is |
|||
#: available on each instance of a sandboxed environment as |
|||
#: :attr:`binop_table` |
|||
default_binop_table = { |
|||
'+': operator.add, |
|||
'-': operator.sub, |
|||
'*': operator.mul, |
|||
'/': operator.truediv, |
|||
'//': operator.floordiv, |
|||
'**': operator.pow, |
|||
'%': operator.mod |
|||
} |
|||
|
|||
#: default callback table for the unary operators. A copy of this is |
|||
#: available on each instance of a sandboxed environment as |
|||
#: :attr:`unop_table` |
|||
default_unop_table = { |
|||
'+': operator.pos, |
|||
'-': operator.neg |
|||
} |
|||
|
|||
#: a set of binary operators that should be intercepted. Each operator |
|||
#: that is added to this set (empty by default) is delegated to the |
|||
#: :meth:`call_binop` method that will perform the operator. The default |
|||
#: operator callback is specified by :attr:`binop_table`. |
|||
#: |
|||
#: The following binary operators are interceptable: |
|||
#: ``//``, ``%``, ``+``, ``*``, ``-``, ``/``, and ``**`` |
|||
#: |
|||
#: The default operation form the operator table corresponds to the |
|||
#: builtin function. Intercepted calls are always slower than the native |
|||
#: operator call, so make sure only to intercept the ones you are |
|||
#: interested in. |
|||
#: |
|||
#: .. versionadded:: 2.6 |
|||
intercepted_binops = frozenset() |
|||
|
|||
#: a set of unary operators that should be intercepted. Each operator |
|||
#: that is added to this set (empty by default) is delegated to the |
|||
#: :meth:`call_unop` method that will perform the operator. The default |
|||
#: operator callback is specified by :attr:`unop_table`. |
|||
#: |
|||
#: The following unary operators are interceptable: ``+``, ``-`` |
|||
#: |
|||
#: The default operation form the operator table corresponds to the |
|||
#: builtin function. Intercepted calls are always slower than the native |
|||
#: operator call, so make sure only to intercept the ones you are |
|||
#: interested in. |
|||
#: |
|||
#: .. versionadded:: 2.6 |
|||
intercepted_unops = frozenset() |
|||
|
|||
def intercept_unop(self, operator): |
|||
"""Called during template compilation with the name of a unary |
|||
operator to check if it should be intercepted at runtime. If this |
|||
method returns `True`, :meth:`call_unop` is excuted for this unary |
|||
operator. The default implementation of :meth:`call_unop` will use |
|||
the :attr:`unop_table` dictionary to perform the operator with the |
|||
same logic as the builtin one. |
|||
|
|||
The following unary operators are interceptable: ``+`` and ``-`` |
|||
|
|||
Intercepted calls are always slower than the native operator call, |
|||
so make sure only to intercept the ones you are interested in. |
|||
|
|||
.. versionadded:: 2.6 |
|||
""" |
|||
return False |
|||
|
|||
|
|||
def __init__(self, *args, **kwargs): |
|||
Environment.__init__(self, *args, **kwargs) |
|||
self.globals['range'] = safe_range |
|||
self.binop_table = self.default_binop_table.copy() |
|||
self.unop_table = self.default_unop_table.copy() |
|||
|
|||
def is_safe_attribute(self, obj, attr, value): |
|||
"""The sandboxed environment will call this method to check if the |
|||
attribute of an object is safe to access. Per default all attributes |
|||
starting with an underscore are considered private as well as the |
|||
special attributes of internal python objects as returned by the |
|||
:func:`is_internal_attribute` function. |
|||
""" |
|||
return not (attr.startswith('_') or is_internal_attribute(obj, attr)) |
|||
|
|||
def is_safe_callable(self, obj): |
|||
"""Check if an object is safely callable. Per default a function is |
|||
considered safe unless the `unsafe_callable` attribute exists and is |
|||
True. Override this method to alter the behavior, but this won't |
|||
affect the `unsafe` decorator from this module. |
|||
""" |
|||
return not (getattr(obj, 'unsafe_callable', False) or |
|||
getattr(obj, 'alters_data', False)) |
|||
|
|||
def call_binop(self, context, operator, left, right): |
|||
"""For intercepted binary operator calls (:meth:`intercepted_binops`) |
|||
this function is executed instead of the builtin operator. This can |
|||
be used to fine tune the behavior of certain operators. |
|||
|
|||
.. versionadded:: 2.6 |
|||
""" |
|||
return self.binop_table[operator](left, right) |
|||
|
|||
def call_unop(self, context, operator, arg): |
|||
"""For intercepted unary operator calls (:meth:`intercepted_unops`) |
|||
this function is executed instead of the builtin operator. This can |
|||
be used to fine tune the behavior of certain operators. |
|||
|
|||
.. versionadded:: 2.6 |
|||
""" |
|||
return self.unop_table[operator](arg) |
|||
|
|||
def getitem(self, obj, argument): |
|||
"""Subscribe an object from sandboxed code.""" |
|||
try: |
|||
return obj[argument] |
|||
except (TypeError, LookupError): |
|||
if isinstance(argument, basestring): |
|||
try: |
|||
attr = str(argument) |
|||
except Exception: |
|||
pass |
|||
else: |
|||
try: |
|||
value = getattr(obj, attr) |
|||
except AttributeError: |
|||
pass |
|||
else: |
|||
if self.is_safe_attribute(obj, argument, value): |
|||
return value |
|||
return self.unsafe_undefined(obj, argument) |
|||
return self.undefined(obj=obj, name=argument) |
|||
|
|||
def getattr(self, obj, attribute): |
|||
"""Subscribe an object from sandboxed code and prefer the |
|||
attribute. The attribute passed *must* be a bytestring. |
|||
""" |
|||
try: |
|||
value = getattr(obj, attribute) |
|||
except AttributeError: |
|||
try: |
|||
return obj[attribute] |
|||
except (TypeError, LookupError): |
|||
pass |
|||
else: |
|||
if self.is_safe_attribute(obj, attribute, value): |
|||
return value |
|||
return self.unsafe_undefined(obj, attribute) |
|||
return self.undefined(obj=obj, name=attribute) |
|||
|
|||
def unsafe_undefined(self, obj, attribute): |
|||
"""Return an undefined object for unsafe attributes.""" |
|||
return self.undefined('access to attribute %r of %r ' |
|||
'object is unsafe.' % ( |
|||
attribute, |
|||
obj.__class__.__name__ |
|||
), name=attribute, obj=obj, exc=SecurityError) |
|||
|
|||
def call(__self, __context, __obj, *args, **kwargs): |
|||
"""Call an object from sandboxed code.""" |
|||
# the double prefixes are to avoid double keyword argument |
|||
# errors when proxying the call. |
|||
if not __self.is_safe_callable(__obj): |
|||
raise SecurityError('%r is not safely callable' % (__obj,)) |
|||
return __context.call(__obj, *args, **kwargs) |
|||
|
|||
|
|||
class ImmutableSandboxedEnvironment(SandboxedEnvironment): |
|||
"""Works exactly like the regular `SandboxedEnvironment` but does not |
|||
permit modifications on the builtin mutable objects `list`, `set`, and |
|||
`dict` by using the :func:`modifies_known_mutable` function. |
|||
""" |
|||
|
|||
def is_safe_attribute(self, obj, attr, value): |
|||
if not SandboxedEnvironment.is_safe_attribute(self, obj, attr, value): |
|||
return False |
|||
return not modifies_known_mutable(obj, attr) |
@ -1,161 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
jinja2.tests |
|||
~~~~~~~~~~~~ |
|||
|
|||
Jinja test functions. Used with the "is" operator. |
|||
|
|||
:copyright: (c) 2010 by the Jinja Team. |
|||
:license: BSD, see LICENSE for more details. |
|||
""" |
|||
import re |
|||
from jinja2.runtime import Undefined |
|||
|
|||
try: |
|||
from collections import Mapping as MappingType |
|||
except ImportError: |
|||
import UserDict |
|||
MappingType = (UserDict.UserDict, UserDict.DictMixin, dict) |
|||
|
|||
# nose, nothing here to test |
|||
__test__ = False |
|||
|
|||
|
|||
number_re = re.compile(r'^-?\d+(\.\d+)?$') |
|||
regex_type = type(number_re) |
|||
|
|||
|
|||
try: |
|||
test_callable = callable |
|||
except NameError: |
|||
def test_callable(x): |
|||
return hasattr(x, '__call__') |
|||
|
|||
|
|||
def test_odd(value): |
|||
"""Return true if the variable is odd.""" |
|||
return value % 2 == 1 |
|||
|
|||
|
|||
def test_even(value): |
|||
"""Return true if the variable is even.""" |
|||
return value % 2 == 0 |
|||
|
|||
|
|||
def test_divisibleby(value, num): |
|||
"""Check if a variable is divisible by a number.""" |
|||
return value % num == 0 |
|||
|
|||
|
|||
def test_defined(value): |
|||
"""Return true if the variable is defined: |
|||
|
|||
.. sourcecode:: jinja |
|||
|
|||
{% if variable is defined %} |
|||
value of variable: {{ variable }} |
|||
{% else %} |
|||
variable is not defined |
|||
{% endif %} |
|||
|
|||
See the :func:`default` filter for a simple way to set undefined |
|||
variables. |
|||
""" |
|||
return not isinstance(value, Undefined) |
|||
|
|||
|
|||
def test_undefined(value): |
|||
"""Like :func:`defined` but the other way round.""" |
|||
return isinstance(value, Undefined) |
|||
|
|||
|
|||
def test_none(value): |
|||
"""Return true if the variable is none.""" |
|||
return value is None |
|||
|
|||
|
|||
def test_lower(value): |
|||
"""Return true if the variable is lowercased.""" |
|||
return unicode(value).islower() |
|||
|
|||
|
|||
def test_upper(value): |
|||
"""Return true if the variable is uppercased.""" |
|||
return unicode(value).isupper() |
|||
|
|||
|
|||
def test_string(value): |
|||
"""Return true if the object is a string.""" |
|||
return isinstance(value, basestring) |
|||
|
|||
|
|||
def test_mapping(value): |
|||
"""Return true if the object is a mapping (dict etc.). |
|||
|
|||
.. versionadded:: 2.6 |
|||
""" |
|||
return isinstance(value, MappingType) |
|||
|
|||
|
|||
def test_number(value): |
|||
"""Return true if the variable is a number.""" |
|||
return isinstance(value, (int, long, float, complex)) |
|||
|
|||
|
|||
def test_sequence(value): |
|||
"""Return true if the variable is a sequence. Sequences are variables |
|||
that are iterable. |
|||
""" |
|||
try: |
|||
len(value) |
|||
value.__getitem__ |
|||
except: |
|||
return False |
|||
return True |
|||
|
|||
|
|||
def test_sameas(value, other): |
|||
"""Check if an object points to the same memory address than another |
|||
object: |
|||
|
|||
.. sourcecode:: jinja |
|||
|
|||
{% if foo.attribute is sameas false %} |
|||
the foo attribute really is the `False` singleton |
|||
{% endif %} |
|||
""" |
|||
return value is other |
|||
|
|||
|
|||
def test_iterable(value): |
|||
"""Check if it's possible to iterate over an object.""" |
|||
try: |
|||
iter(value) |
|||
except TypeError: |
|||
return False |
|||
return True |
|||
|
|||
|
|||
def test_escaped(value): |
|||
"""Check if the value is escaped.""" |
|||
return hasattr(value, '__html__') |
|||
|
|||
|
|||
TESTS = { |
|||
'odd': test_odd, |
|||
'even': test_even, |
|||
'divisibleby': test_divisibleby, |
|||
'defined': test_defined, |
|||
'undefined': test_undefined, |
|||
'none': test_none, |
|||
'lower': test_lower, |
|||
'upper': test_upper, |
|||
'string': test_string, |
|||
'mapping': test_mapping, |
|||
'number': test_number, |
|||
'sequence': test_sequence, |
|||
'iterable': test_iterable, |
|||
'callable': test_callable, |
|||
'sameas': test_sameas, |
|||
'escaped': test_escaped |
|||
} |
@ -1,95 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
jinja2.testsuite |
|||
~~~~~~~~~~~~~~~~ |
|||
|
|||
All the unittests of Jinja2. These tests can be executed by |
|||
either running run-tests.py using multiple Python versions at |
|||
the same time. |
|||
|
|||
:copyright: (c) 2010 by the Jinja Team. |
|||
:license: BSD, see LICENSE for more details. |
|||
""" |
|||
import os |
|||
import re |
|||
import sys |
|||
import unittest |
|||
from traceback import format_exception |
|||
from jinja2 import loaders |
|||
|
|||
|
|||
here = os.path.dirname(os.path.abspath(__file__)) |
|||
|
|||
dict_loader = loaders.DictLoader({ |
|||
'justdict.html': 'FOO' |
|||
}) |
|||
package_loader = loaders.PackageLoader('jinja2.testsuite.res', 'templates') |
|||
filesystem_loader = loaders.FileSystemLoader(here + '/res/templates') |
|||
function_loader = loaders.FunctionLoader({'justfunction.html': 'FOO'}.get) |
|||
choice_loader = loaders.ChoiceLoader([dict_loader, package_loader]) |
|||
prefix_loader = loaders.PrefixLoader({ |
|||
'a': filesystem_loader, |
|||
'b': dict_loader |
|||
}) |
|||
|
|||
|
|||
class JinjaTestCase(unittest.TestCase): |
|||
|
|||
### use only these methods for testing. If you need standard |
|||
### unittest method, wrap them! |
|||
|
|||
def setup(self): |
|||
pass |
|||
|
|||
def teardown(self): |
|||
pass |
|||
|
|||
def setUp(self): |
|||
self.setup() |
|||
|
|||
def tearDown(self): |
|||
self.teardown() |
|||
|
|||
def assert_equal(self, a, b): |
|||
return self.assertEqual(a, b) |
|||
|
|||
def assert_raises(self, *args, **kwargs): |
|||
return self.assertRaises(*args, **kwargs) |
|||
|
|||
def assert_traceback_matches(self, callback, expected_tb): |
|||
try: |
|||
callback() |
|||
except Exception, e: |
|||
tb = format_exception(*sys.exc_info()) |
|||
if re.search(expected_tb.strip(), ''.join(tb)) is None: |
|||
raise self.fail('Traceback did not match:\n\n%s\nexpected:\n%s' |
|||
% (''.join(tb), expected_tb)) |
|||
else: |
|||
self.fail('Expected exception') |
|||
|
|||
|
|||
def suite(): |
|||
from jinja2.testsuite import ext, filters, tests, core_tags, \ |
|||
loader, inheritance, imports, lexnparse, security, api, \ |
|||
regression, debug, utils, doctests |
|||
suite = unittest.TestSuite() |
|||
suite.addTest(ext.suite()) |
|||
suite.addTest(filters.suite()) |
|||
suite.addTest(tests.suite()) |
|||
suite.addTest(core_tags.suite()) |
|||
suite.addTest(loader.suite()) |
|||
suite.addTest(inheritance.suite()) |
|||
suite.addTest(imports.suite()) |
|||
suite.addTest(lexnparse.suite()) |
|||
suite.addTest(security.suite()) |
|||
suite.addTest(api.suite()) |
|||
suite.addTest(regression.suite()) |
|||
suite.addTest(debug.suite()) |
|||
suite.addTest(utils.suite()) |
|||
|
|||
# doctests will not run on python 3 currently. Too many issues |
|||
# with that, do not test that on that platform. |
|||
if sys.version_info < (3, 0): |
|||
suite.addTest(doctests.suite()) |
|||
|
|||
return suite |
@ -1,245 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
jinja2.testsuite.api |
|||
~~~~~~~~~~~~~~~~~~~~ |
|||
|
|||
Tests the public API and related stuff. |
|||
|
|||
:copyright: (c) 2010 by the Jinja Team. |
|||
:license: BSD, see LICENSE for more details. |
|||
""" |
|||
import unittest |
|||
|
|||
from jinja2.testsuite import JinjaTestCase |
|||
|
|||
from jinja2 import Environment, Undefined, DebugUndefined, \ |
|||
StrictUndefined, UndefinedError, meta, \ |
|||
is_undefined, Template, DictLoader |
|||
from jinja2.utils import Cycler |
|||
|
|||
env = Environment() |
|||
|
|||
|
|||
class ExtendedAPITestCase(JinjaTestCase): |
|||
|
|||
def test_item_and_attribute(self): |
|||
from jinja2.sandbox import SandboxedEnvironment |
|||
|
|||
for env in Environment(), SandboxedEnvironment(): |
|||
# the |list is necessary for python3 |
|||
tmpl = env.from_string('{{ foo.items()|list }}') |
|||
assert tmpl.render(foo={'items': 42}) == "[('items', 42)]" |
|||
tmpl = env.from_string('{{ foo|attr("items")()|list }}') |
|||
assert tmpl.render(foo={'items': 42}) == "[('items', 42)]" |
|||
tmpl = env.from_string('{{ foo["items"] }}') |
|||
assert tmpl.render(foo={'items': 42}) == '42' |
|||
|
|||
def test_finalizer(self): |
|||
def finalize_none_empty(value): |
|||
if value is None: |
|||
value = u'' |
|||
return value |
|||
env = Environment(finalize=finalize_none_empty) |
|||
tmpl = env.from_string('{% for item in seq %}|{{ item }}{% endfor %}') |
|||
assert tmpl.render(seq=(None, 1, "foo")) == '||1|foo' |
|||
tmpl = env.from_string('<{{ none }}>') |
|||
assert tmpl.render() == '<>' |
|||
|
|||
def test_cycler(self): |
|||
items = 1, 2, 3 |
|||
c = Cycler(*items) |
|||
for item in items + items: |
|||
assert c.current == item |
|||
assert c.next() == item |
|||
c.next() |
|||
assert c.current == 2 |
|||
c.reset() |
|||
assert c.current == 1 |
|||
|
|||
def test_expressions(self): |
|||
expr = env.compile_expression("foo") |
|||
assert expr() is None |
|||
assert expr(foo=42) == 42 |
|||
expr2 = env.compile_expression("foo", undefined_to_none=False) |
|||
assert is_undefined(expr2()) |
|||
|
|||
expr = env.compile_expression("42 + foo") |
|||
assert expr(foo=42) == 84 |
|||
|
|||
def test_template_passthrough(self): |
|||
t = Template('Content') |
|||
assert env.get_template(t) is t |
|||
assert env.select_template([t]) is t |
|||
assert env.get_or_select_template([t]) is t |
|||
assert env.get_or_select_template(t) is t |
|||
|
|||
def test_autoescape_autoselect(self): |
|||
def select_autoescape(name): |
|||
if name is None or '.' not in name: |
|||
return False |
|||
return name.endswith('.html') |
|||
env = Environment(autoescape=select_autoescape, |
|||
loader=DictLoader({ |
|||
'test.txt': '{{ foo }}', |
|||
'test.html': '{{ foo }}' |
|||
})) |
|||
t = env.get_template('test.txt') |
|||
assert t.render(foo='<foo>') == '<foo>' |
|||
t = env.get_template('test.html') |
|||
assert t.render(foo='<foo>') == '<foo>' |
|||
t = env.from_string('{{ foo }}') |
|||
assert t.render(foo='<foo>') == '<foo>' |
|||
|
|||
|
|||
class MetaTestCase(JinjaTestCase): |
|||
|
|||
def test_find_undeclared_variables(self): |
|||
ast = env.parse('{% set foo = 42 %}{{ bar + foo }}') |
|||
x = meta.find_undeclared_variables(ast) |
|||
assert x == set(['bar']) |
|||
|
|||
ast = env.parse('{% set foo = 42 %}{{ bar + foo }}' |
|||
'{% macro meh(x) %}{{ x }}{% endmacro %}' |
|||
'{% for item in seq %}{{ muh(item) + meh(seq) }}{% endfor %}') |
|||
x = meta.find_undeclared_variables(ast) |
|||
assert x == set(['bar', 'seq', 'muh']) |
|||
|
|||
def test_find_refererenced_templates(self): |
|||
ast = env.parse('{% extends "layout.html" %}{% include helper %}') |
|||
i = meta.find_referenced_templates(ast) |
|||
assert i.next() == 'layout.html' |
|||
assert i.next() is None |
|||
assert list(i) == [] |
|||
|
|||
ast = env.parse('{% extends "layout.html" %}' |
|||
'{% from "test.html" import a, b as c %}' |
|||
'{% import "meh.html" as meh %}' |
|||
'{% include "muh.html" %}') |
|||
i = meta.find_referenced_templates(ast) |
|||
assert list(i) == ['layout.html', 'test.html', 'meh.html', 'muh.html'] |
|||
|
|||
def test_find_included_templates(self): |
|||
ast = env.parse('{% include ["foo.html", "bar.html"] %}') |
|||
i = meta.find_referenced_templates(ast) |
|||
assert list(i) == ['foo.html', 'bar.html'] |
|||
|
|||
ast = env.parse('{% include ("foo.html", "bar.html") %}') |
|||
i = meta.find_referenced_templates(ast) |
|||
assert list(i) == ['foo.html', 'bar.html'] |
|||
|
|||
ast = env.parse('{% include ["foo.html", "bar.html", foo] %}') |
|||
i = meta.find_referenced_templates(ast) |
|||
assert list(i) == ['foo.html', 'bar.html', None] |
|||
|
|||
ast = env.parse('{% include ("foo.html", "bar.html", foo) %}') |
|||
i = meta.find_referenced_templates(ast) |
|||
assert list(i) == ['foo.html', 'bar.html', None] |
|||
|
|||
|
|||
class StreamingTestCase(JinjaTestCase): |
|||
|
|||
def test_basic_streaming(self): |
|||
tmpl = env.from_string("<ul>{% for item in seq %}<li>{{ loop.index " |
|||
"}} - {{ item }}</li>{%- endfor %}</ul>") |
|||
stream = tmpl.stream(seq=range(4)) |
|||
self.assert_equal(stream.next(), '<ul>') |
|||
self.assert_equal(stream.next(), '<li>1 - 0</li>') |
|||
self.assert_equal(stream.next(), '<li>2 - 1</li>') |
|||
self.assert_equal(stream.next(), '<li>3 - 2</li>') |
|||
self.assert_equal(stream.next(), '<li>4 - 3</li>') |
|||
self.assert_equal(stream.next(), '</ul>') |
|||
|
|||
def test_buffered_streaming(self): |
|||
tmpl = env.from_string("<ul>{% for item in seq %}<li>{{ loop.index " |
|||
"}} - {{ item }}</li>{%- endfor %}</ul>") |
|||
stream = tmpl.stream(seq=range(4)) |
|||
stream.enable_buffering(size=3) |
|||
self.assert_equal(stream.next(), u'<ul><li>1 - 0</li><li>2 - 1</li>') |
|||
self.assert_equal(stream.next(), u'<li>3 - 2</li><li>4 - 3</li></ul>') |
|||
|
|||
def test_streaming_behavior(self): |
|||
tmpl = env.from_string("") |
|||
stream = tmpl.stream() |
|||
assert not stream.buffered |
|||
stream.enable_buffering(20) |
|||
assert stream.buffered |
|||
stream.disable_buffering() |
|||
assert not stream.buffered |
|||
|
|||
|
|||
class UndefinedTestCase(JinjaTestCase): |
|||
|
|||
def test_stopiteration_is_undefined(self): |
|||
def test(): |
|||
raise StopIteration() |
|||
t = Template('A{{ test() }}B') |
|||
assert t.render(test=test) == 'AB' |
|||
t = Template('A{{ test().missingattribute }}B') |
|||
self.assert_raises(UndefinedError, t.render, test=test) |
|||
|
|||
def test_undefined_and_special_attributes(self): |
|||
try: |
|||
Undefined('Foo').__dict__ |
|||
except AttributeError: |
|||
pass |
|||
else: |
|||
assert False, "Expected actual attribute error" |
|||
|
|||
def test_default_undefined(self): |
|||
env = Environment(undefined=Undefined) |
|||
self.assert_equal(env.from_string('{{ missing }}').render(), u'') |
|||
self.assert_raises(UndefinedError, |
|||
env.from_string('{{ missing.attribute }}').render) |
|||
self.assert_equal(env.from_string('{{ missing|list }}').render(), '[]') |
|||
self.assert_equal(env.from_string('{{ missing is not defined }}').render(), 'True') |
|||
self.assert_equal(env.from_string('{{ foo.missing }}').render(foo=42), '') |
|||
self.assert_equal(env.from_string('{{ not missing }}').render(), 'True') |
|||
|
|||
def test_debug_undefined(self): |
|||
env = Environment(undefined=DebugUndefined) |
|||
self.assert_equal(env.from_string('{{ missing }}').render(), '{{ missing }}') |
|||
self.assert_raises(UndefinedError, |
|||
env.from_string('{{ missing.attribute }}').render) |
|||
self.assert_equal(env.from_string('{{ missing|list }}').render(), '[]') |
|||
self.assert_equal(env.from_string('{{ missing is not defined }}').render(), 'True') |
|||
self.assert_equal(env.from_string('{{ foo.missing }}').render(foo=42), |
|||
u"{{ no such element: int object['missing'] }}") |
|||
self.assert_equal(env.from_string('{{ not missing }}').render(), 'True') |
|||
|
|||
def test_strict_undefined(self): |
|||
env = Environment(undefined=StrictUndefined) |
|||
self.assert_raises(UndefinedError, env.from_string('{{ missing }}').render) |
|||
self.assert_raises(UndefinedError, env.from_string('{{ missing.attribute }}').render) |
|||
self.assert_raises(UndefinedError, env.from_string('{{ missing|list }}').render) |
|||
self.assert_equal(env.from_string('{{ missing is not defined }}').render(), 'True') |
|||
self.assert_raises(UndefinedError, env.from_string('{{ foo.missing }}').render, foo=42) |
|||
self.assert_raises(UndefinedError, env.from_string('{{ not missing }}').render) |
|||
|
|||
def test_indexing_gives_undefined(self): |
|||
t = Template("{{ var[42].foo }}") |
|||
self.assert_raises(UndefinedError, t.render, var=0) |
|||
|
|||
def test_none_gives_proper_error(self): |
|||
try: |
|||
Environment().getattr(None, 'split')() |
|||
except UndefinedError, e: |
|||
assert e.message == "'None' has no attribute 'split'" |
|||
else: |
|||
assert False, 'expected exception' |
|||
|
|||
def test_object_repr(self): |
|||
try: |
|||
Undefined(obj=42, name='upper')() |
|||
except UndefinedError, e: |
|||
assert e.message == "'int object' has no attribute 'upper'" |
|||
else: |
|||
assert False, 'expected exception' |
|||
|
|||
|
|||
def suite(): |
|||
suite = unittest.TestSuite() |
|||
suite.addTest(unittest.makeSuite(ExtendedAPITestCase)) |
|||
suite.addTest(unittest.makeSuite(MetaTestCase)) |
|||
suite.addTest(unittest.makeSuite(StreamingTestCase)) |
|||
suite.addTest(unittest.makeSuite(UndefinedTestCase)) |
|||
return suite |
@ -1,285 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
jinja2.testsuite.core_tags |
|||
~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|||
|
|||
Test the core tags like for and if. |
|||
|
|||
:copyright: (c) 2010 by the Jinja Team. |
|||
:license: BSD, see LICENSE for more details. |
|||
""" |
|||
import unittest |
|||
|
|||
from jinja2.testsuite import JinjaTestCase |
|||
|
|||
from jinja2 import Environment, TemplateSyntaxError, UndefinedError, \ |
|||
DictLoader |
|||
|
|||
env = Environment() |
|||
|
|||
|
|||
class ForLoopTestCase(JinjaTestCase): |
|||
|
|||
def test_simple(self): |
|||
tmpl = env.from_string('{% for item in seq %}{{ item }}{% endfor %}') |
|||
assert tmpl.render(seq=range(10)) == '0123456789' |
|||
|
|||
def test_else(self): |
|||
tmpl = env.from_string('{% for item in seq %}XXX{% else %}...{% endfor %}') |
|||
assert tmpl.render() == '...' |
|||
|
|||
def test_empty_blocks(self): |
|||
tmpl = env.from_string('<{% for item in seq %}{% else %}{% endfor %}>') |
|||
assert tmpl.render() == '<>' |
|||
|
|||
def test_context_vars(self): |
|||
tmpl = env.from_string('''{% for item in seq -%} |
|||
{{ loop.index }}|{{ loop.index0 }}|{{ loop.revindex }}|{{ |
|||
loop.revindex0 }}|{{ loop.first }}|{{ loop.last }}|{{ |
|||
loop.length }}###{% endfor %}''') |
|||
one, two, _ = tmpl.render(seq=[0, 1]).split('###') |
|||
(one_index, one_index0, one_revindex, one_revindex0, one_first, |
|||
one_last, one_length) = one.split('|') |
|||
(two_index, two_index0, two_revindex, two_revindex0, two_first, |
|||
two_last, two_length) = two.split('|') |
|||
|
|||
assert int(one_index) == 1 and int(two_index) == 2 |
|||
assert int(one_index0) == 0 and int(two_index0) == 1 |
|||
assert int(one_revindex) == 2 and int(two_revindex) == 1 |
|||
assert int(one_revindex0) == 1 and int(two_revindex0) == 0 |
|||
assert one_first == 'True' and two_first == 'False' |
|||
assert one_last == 'False' and two_last == 'True' |
|||
assert one_length == two_length == '2' |
|||
|
|||
def test_cycling(self): |
|||
tmpl = env.from_string('''{% for item in seq %}{{ |
|||
loop.cycle('<1>', '<2>') }}{% endfor %}{% |
|||
for item in seq %}{{ loop.cycle(*through) }}{% endfor %}''') |
|||
output = tmpl.render(seq=range(4), through=('<1>', '<2>')) |
|||
assert output == '<1><2>' * 4 |
|||
|
|||
def test_scope(self): |
|||
tmpl = env.from_string('{% for item in seq %}{% endfor %}{{ item }}') |
|||
output = tmpl.render(seq=range(10)) |
|||
assert not output |
|||
|
|||
def test_varlen(self): |
|||
def inner(): |
|||
for item in range(5): |
|||
yield item |
|||
tmpl = env.from_string('{% for item in iter %}{{ item }}{% endfor %}') |
|||
output = tmpl.render(iter=inner()) |
|||
assert output == '01234' |
|||
|
|||
def test_noniter(self): |
|||
tmpl = env.from_string('{% for item in none %}...{% endfor %}') |
|||
self.assert_raises(TypeError, tmpl.render) |
|||
|
|||
def test_recursive(self): |
|||
tmpl = env.from_string('''{% for item in seq recursive -%} |
|||
[{{ item.a }}{% if item.b %}<{{ loop(item.b) }}>{% endif %}] |
|||
{%- endfor %}''') |
|||
assert tmpl.render(seq=[ |
|||
dict(a=1, b=[dict(a=1), dict(a=2)]), |
|||
dict(a=2, b=[dict(a=1), dict(a=2)]), |
|||
dict(a=3, b=[dict(a='a')]) |
|||
]) == '[1<[1][2]>][2<[1][2]>][3<[a]>]' |
|||
|
|||
def test_looploop(self): |
|||
tmpl = env.from_string('''{% for row in table %} |
|||
{%- set rowloop = loop -%} |
|||
{% for cell in row -%} |
|||
[{{ rowloop.index }}|{{ loop.index }}] |
|||
{%- endfor %} |
|||
{%- endfor %}''') |
|||
assert tmpl.render(table=['ab', 'cd']) == '[1|1][1|2][2|1][2|2]' |
|||
|
|||
def test_reversed_bug(self): |
|||
tmpl = env.from_string('{% for i in items %}{{ i }}' |
|||
'{% if not loop.last %}' |
|||
',{% endif %}{% endfor %}') |
|||
assert tmpl.render(items=reversed([3, 2, 1])) == '1,2,3' |
|||
|
|||
def test_loop_errors(self): |
|||
tmpl = env.from_string('''{% for item in [1] if loop.index |
|||
== 0 %}...{% endfor %}''') |
|||
self.assert_raises(UndefinedError, tmpl.render) |
|||
tmpl = env.from_string('''{% for item in [] %}...{% else |
|||
%}{{ loop }}{% endfor %}''') |
|||
assert tmpl.render() == '' |
|||
|
|||
def test_loop_filter(self): |
|||
tmpl = env.from_string('{% for item in range(10) if item ' |
|||
'is even %}[{{ item }}]{% endfor %}') |
|||
assert tmpl.render() == '[0][2][4][6][8]' |
|||
tmpl = env.from_string(''' |
|||
{%- for item in range(10) if item is even %}[{{ |
|||
loop.index }}:{{ item }}]{% endfor %}''') |
|||
assert tmpl.render() == '[1:0][2:2][3:4][4:6][5:8]' |
|||
|
|||
def test_loop_unassignable(self): |
|||
self.assert_raises(TemplateSyntaxError, env.from_string, |
|||
'{% for loop in seq %}...{% endfor %}') |
|||
|
|||
def test_scoped_special_var(self): |
|||
t = env.from_string('{% for s in seq %}[{{ loop.first }}{% for c in s %}' |
|||
'|{{ loop.first }}{% endfor %}]{% endfor %}') |
|||
assert t.render(seq=('ab', 'cd')) == '[True|True|False][False|True|False]' |
|||
|
|||
def test_scoped_loop_var(self): |
|||
t = env.from_string('{% for x in seq %}{{ loop.first }}' |
|||
'{% for y in seq %}{% endfor %}{% endfor %}') |
|||
assert t.render(seq='ab') == 'TrueFalse' |
|||
t = env.from_string('{% for x in seq %}{% for y in seq %}' |
|||
'{{ loop.first }}{% endfor %}{% endfor %}') |
|||
assert t.render(seq='ab') == 'TrueFalseTrueFalse' |
|||
|
|||
def test_recursive_empty_loop_iter(self): |
|||
t = env.from_string(''' |
|||
{%- for item in foo recursive -%}{%- endfor -%} |
|||
''') |
|||
assert t.render(dict(foo=[])) == '' |
|||
|
|||
def test_call_in_loop(self): |
|||
t = env.from_string(''' |
|||
{%- macro do_something() -%} |
|||
[{{ caller() }}] |
|||
{%- endmacro %} |
|||
|
|||
{%- for i in [1, 2, 3] %} |
|||
{%- call do_something() -%} |
|||
{{ i }} |
|||
{%- endcall %} |
|||
{%- endfor -%} |
|||
''') |
|||
assert t.render() == '[1][2][3]' |
|||
|
|||
def test_scoping_bug(self): |
|||
t = env.from_string(''' |
|||
{%- for item in foo %}...{{ item }}...{% endfor %} |
|||
{%- macro item(a) %}...{{ a }}...{% endmacro %} |
|||
{{- item(2) -}} |
|||
''') |
|||
assert t.render(foo=(1,)) == '...1......2...' |
|||
|
|||
def test_unpacking(self): |
|||
tmpl = env.from_string('{% for a, b, c in [[1, 2, 3]] %}' |
|||
'{{ a }}|{{ b }}|{{ c }}{% endfor %}') |
|||
assert tmpl.render() == '1|2|3' |
|||
|
|||
|
|||
class IfConditionTestCase(JinjaTestCase): |
|||
|
|||
def test_simple(self): |
|||
tmpl = env.from_string('''{% if true %}...{% endif %}''') |
|||
assert tmpl.render() == '...' |
|||
|
|||
def test_elif(self): |
|||
tmpl = env.from_string('''{% if false %}XXX{% elif true |
|||
%}...{% else %}XXX{% endif %}''') |
|||
assert tmpl.render() == '...' |
|||
|
|||
def test_else(self): |
|||
tmpl = env.from_string('{% if false %}XXX{% else %}...{% endif %}') |
|||
assert tmpl.render() == '...' |
|||
|
|||
def test_empty(self): |
|||
tmpl = env.from_string('[{% if true %}{% else %}{% endif %}]') |
|||
assert tmpl.render() == '[]' |
|||
|
|||
def test_complete(self): |
|||
tmpl = env.from_string('{% if a %}A{% elif b %}B{% elif c == d %}' |
|||
'C{% else %}D{% endif %}') |
|||
assert tmpl.render(a=0, b=False, c=42, d=42.0) == 'C' |
|||
|
|||
def test_no_scope(self): |
|||
tmpl = env.from_string('{% if a %}{% set foo = 1 %}{% endif %}{{ foo }}') |
|||
assert tmpl.render(a=True) == '1' |
|||
tmpl = env.from_string('{% if true %}{% set foo = 1 %}{% endif %}{{ foo }}') |
|||
assert tmpl.render() == '1' |
|||
|
|||
|
|||
class MacrosTestCase(JinjaTestCase): |
|||
env = Environment(trim_blocks=True) |
|||
|
|||
def test_simple(self): |
|||
tmpl = self.env.from_string('''\ |
|||
{% macro say_hello(name) %}Hello {{ name }}!{% endmacro %} |
|||
{{ say_hello('Peter') }}''') |
|||
assert tmpl.render() == 'Hello Peter!' |
|||
|
|||
def test_scoping(self): |
|||
tmpl = self.env.from_string('''\ |
|||
{% macro level1(data1) %} |
|||
{% macro level2(data2) %}{{ data1 }}|{{ data2 }}{% endmacro %} |
|||
{{ level2('bar') }}{% endmacro %} |
|||
{{ level1('foo') }}''') |
|||
assert tmpl.render() == 'foo|bar' |
|||
|
|||
def test_arguments(self): |
|||
tmpl = self.env.from_string('''\ |
|||
{% macro m(a, b, c='c', d='d') %}{{ a }}|{{ b }}|{{ c }}|{{ d }}{% endmacro %} |
|||
{{ m() }}|{{ m('a') }}|{{ m('a', 'b') }}|{{ m(1, 2, 3) }}''') |
|||
assert tmpl.render() == '||c|d|a||c|d|a|b|c|d|1|2|3|d' |
|||
|
|||
def test_varargs(self): |
|||
tmpl = self.env.from_string('''\ |
|||
{% macro test() %}{{ varargs|join('|') }}{% endmacro %}\ |
|||
{{ test(1, 2, 3) }}''') |
|||
assert tmpl.render() == '1|2|3' |
|||
|
|||
def test_simple_call(self): |
|||
tmpl = self.env.from_string('''\ |
|||
{% macro test() %}[[{{ caller() }}]]{% endmacro %}\ |
|||
{% call test() %}data{% endcall %}''') |
|||
assert tmpl.render() == '[[data]]' |
|||
|
|||
def test_complex_call(self): |
|||
tmpl = self.env.from_string('''\ |
|||
{% macro test() %}[[{{ caller('data') }}]]{% endmacro %}\ |
|||
{% call(data) test() %}{{ data }}{% endcall %}''') |
|||
assert tmpl.render() == '[[data]]' |
|||
|
|||
def test_caller_undefined(self): |
|||
tmpl = self.env.from_string('''\ |
|||
{% set caller = 42 %}\ |
|||
{% macro test() %}{{ caller is not defined }}{% endmacro %}\ |
|||
{{ test() }}''') |
|||
assert tmpl.render() == 'True' |
|||
|
|||
def test_include(self): |
|||
self.env = Environment(loader=DictLoader({'include': |
|||
'{% macro test(foo) %}[{{ foo }}]{% endmacro %}'})) |
|||
tmpl = self.env.from_string('{% from "include" import test %}{{ test("foo") }}') |
|||
assert tmpl.render() == '[foo]' |
|||
|
|||
def test_macro_api(self): |
|||
tmpl = self.env.from_string('{% macro foo(a, b) %}{% endmacro %}' |
|||
'{% macro bar() %}{{ varargs }}{{ kwargs }}{% endmacro %}' |
|||
'{% macro baz() %}{{ caller() }}{% endmacro %}') |
|||
assert tmpl.module.foo.arguments == ('a', 'b') |
|||
assert tmpl.module.foo.defaults == () |
|||
assert tmpl.module.foo.name == 'foo' |
|||
assert not tmpl.module.foo.caller |
|||
assert not tmpl.module.foo.catch_kwargs |
|||
assert not tmpl.module.foo.catch_varargs |
|||
assert tmpl.module.bar.arguments == () |
|||
assert tmpl.module.bar.defaults == () |
|||
assert not tmpl.module.bar.caller |
|||
assert tmpl.module.bar.catch_kwargs |
|||
assert tmpl.module.bar.catch_varargs |
|||
assert tmpl.module.baz.caller |
|||
|
|||
def test_callself(self): |
|||
tmpl = self.env.from_string('{% macro foo(x) %}{{ x }}{% if x > 1 %}|' |
|||
'{{ foo(x - 1) }}{% endif %}{% endmacro %}' |
|||
'{{ foo(5) }}') |
|||
assert tmpl.render() == '5|4|3|2|1' |
|||
|
|||
|
|||
def suite(): |
|||
suite = unittest.TestSuite() |
|||
suite.addTest(unittest.makeSuite(ForLoopTestCase)) |
|||
suite.addTest(unittest.makeSuite(IfConditionTestCase)) |
|||
suite.addTest(unittest.makeSuite(MacrosTestCase)) |
|||
return suite |
@ -1,60 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
jinja2.testsuite.debug |
|||
~~~~~~~~~~~~~~~~~~~~~~ |
|||
|
|||
Tests the debug system. |
|||
|
|||
:copyright: (c) 2010 by the Jinja Team. |
|||
:license: BSD, see LICENSE for more details. |
|||
""" |
|||
import sys |
|||
import unittest |
|||
|
|||
from jinja2.testsuite import JinjaTestCase, filesystem_loader |
|||
|
|||
from jinja2 import Environment, TemplateSyntaxError |
|||
|
|||
env = Environment(loader=filesystem_loader) |
|||
|
|||
|
|||
class DebugTestCase(JinjaTestCase): |
|||
|
|||
if sys.version_info[:2] != (2, 4): |
|||
def test_runtime_error(self): |
|||
def test(): |
|||
tmpl.render(fail=lambda: 1 / 0) |
|||
tmpl = env.get_template('broken.html') |
|||
self.assert_traceback_matches(test, r''' |
|||
File ".*?broken.html", line 2, in (top-level template code|<module>) |
|||
\{\{ fail\(\) \}\} |
|||
File ".*?debug.pyc?", line \d+, in <lambda> |
|||
tmpl\.render\(fail=lambda: 1 / 0\) |
|||
ZeroDivisionError: (int(eger)? )?division (or modulo )?by zero |
|||
''') |
|||
|
|||
def test_syntax_error(self): |
|||
# XXX: the .*? is necessary for python3 which does not hide |
|||
# some of the stack frames we don't want to show. Not sure |
|||
# what's up with that, but that is not that critical. Should |
|||
# be fixed though. |
|||
self.assert_traceback_matches(lambda: env.get_template('syntaxerror.html'), r'''(?sm) |
|||
File ".*?syntaxerror.html", line 4, in (template|<module>) |
|||
\{% endif %\}.*? |
|||
(jinja2\.exceptions\.)?TemplateSyntaxError: Encountered unknown tag 'endif'. Jinja was looking for the following tags: 'endfor' or 'else'. The innermost block that needs to be closed is 'for'. |
|||
''') |
|||
|
|||
def test_regular_syntax_error(self): |
|||
def test(): |
|||
raise TemplateSyntaxError('wtf', 42) |
|||
self.assert_traceback_matches(test, r''' |
|||
File ".*debug.pyc?", line \d+, in test |
|||
raise TemplateSyntaxError\('wtf', 42\) |
|||
(jinja2\.exceptions\.)?TemplateSyntaxError: wtf |
|||
line 42''') |
|||
|
|||
|
|||
def suite(): |
|||
suite = unittest.TestSuite() |
|||
suite.addTest(unittest.makeSuite(DebugTestCase)) |
|||
return suite |
@ -1,29 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
jinja2.testsuite.doctests |
|||
~~~~~~~~~~~~~~~~~~~~~~~~~ |
|||
|
|||
The doctests. Collects all tests we want to test from |
|||
the Jinja modules. |
|||
|
|||
:copyright: (c) 2010 by the Jinja Team. |
|||
:license: BSD, see LICENSE for more details. |
|||
""" |
|||
import unittest |
|||
import doctest |
|||
|
|||
|
|||
def suite(): |
|||
from jinja2 import utils, sandbox, runtime, meta, loaders, \ |
|||
ext, environment, bccache, nodes |
|||
suite = unittest.TestSuite() |
|||
suite.addTest(doctest.DocTestSuite(utils)) |
|||
suite.addTest(doctest.DocTestSuite(sandbox)) |
|||
suite.addTest(doctest.DocTestSuite(runtime)) |
|||
suite.addTest(doctest.DocTestSuite(meta)) |
|||
suite.addTest(doctest.DocTestSuite(loaders)) |
|||
suite.addTest(doctest.DocTestSuite(ext)) |
|||
suite.addTest(doctest.DocTestSuite(environment)) |
|||
suite.addTest(doctest.DocTestSuite(bccache)) |
|||
suite.addTest(doctest.DocTestSuite(nodes)) |
|||
return suite |
@ -1,455 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
jinja2.testsuite.ext |
|||
~~~~~~~~~~~~~~~~~~~~ |
|||
|
|||
Tests for the extensions. |
|||
|
|||
:copyright: (c) 2010 by the Jinja Team. |
|||
:license: BSD, see LICENSE for more details. |
|||
""" |
|||
import re |
|||
import unittest |
|||
|
|||
from jinja2.testsuite import JinjaTestCase |
|||
|
|||
from jinja2 import Environment, DictLoader, contextfunction, nodes |
|||
from jinja2.exceptions import TemplateAssertionError |
|||
from jinja2.ext import Extension |
|||
from jinja2.lexer import Token, count_newlines |
|||
from jinja2.utils import next |
|||
|
|||
# 2.x / 3.x |
|||
try: |
|||
from io import BytesIO |
|||
except ImportError: |
|||
from StringIO import StringIO as BytesIO |
|||
|
|||
|
|||
importable_object = 23 |
|||
|
|||
_gettext_re = re.compile(r'_\((.*?)\)(?s)') |
|||
|
|||
|
|||
i18n_templates = { |
|||
'master.html': '<title>{{ page_title|default(_("missing")) }}</title>' |
|||
'{% block body %}{% endblock %}', |
|||
'child.html': '{% extends "master.html" %}{% block body %}' |
|||
'{% trans %}watch out{% endtrans %}{% endblock %}', |
|||
'plural.html': '{% trans user_count %}One user online{% pluralize %}' |
|||
'{{ user_count }} users online{% endtrans %}', |
|||
'stringformat.html': '{{ _("User: %(num)s")|format(num=user_count) }}' |
|||
} |
|||
|
|||
newstyle_i18n_templates = { |
|||
'master.html': '<title>{{ page_title|default(_("missing")) }}</title>' |
|||
'{% block body %}{% endblock %}', |
|||
'child.html': '{% extends "master.html" %}{% block body %}' |
|||
'{% trans %}watch out{% endtrans %}{% endblock %}', |
|||
'plural.html': '{% trans user_count %}One user online{% pluralize %}' |
|||
'{{ user_count }} users online{% endtrans %}', |
|||
'stringformat.html': '{{ _("User: %(num)s", num=user_count) }}', |
|||
'ngettext.html': '{{ ngettext("%(num)s apple", "%(num)s apples", apples) }}', |
|||
'ngettext_long.html': '{% trans num=apples %}{{ num }} apple{% pluralize %}' |
|||
'{{ num }} apples{% endtrans %}', |
|||
'transvars1.html': '{% trans %}User: {{ num }}{% endtrans %}', |
|||
'transvars2.html': '{% trans num=count %}User: {{ num }}{% endtrans %}', |
|||
'transvars3.html': '{% trans count=num %}User: {{ count }}{% endtrans %}', |
|||
'novars.html': '{% trans %}%(hello)s{% endtrans %}', |
|||
'vars.html': '{% trans %}{{ foo }}%(foo)s{% endtrans %}', |
|||
'explicitvars.html': '{% trans foo="42" %}%(foo)s{% endtrans %}' |
|||
} |
|||
|
|||
|
|||
languages = { |
|||
'de': { |
|||
'missing': u'fehlend', |
|||
'watch out': u'pass auf', |
|||
'One user online': u'Ein Benutzer online', |
|||
'%(user_count)s users online': u'%(user_count)s Benutzer online', |
|||
'User: %(num)s': u'Benutzer: %(num)s', |
|||
'User: %(count)s': u'Benutzer: %(count)s', |
|||
'%(num)s apple': u'%(num)s Apfel', |
|||
'%(num)s apples': u'%(num)s Äpfel' |
|||
} |
|||
} |
|||
|
|||
|
|||
@contextfunction |
|||
def gettext(context, string): |
|||
language = context.get('LANGUAGE', 'en') |
|||
return languages.get(language, {}).get(string, string) |
|||
|
|||
|
|||
@contextfunction |
|||
def ngettext(context, s, p, n): |
|||
language = context.get('LANGUAGE', 'en') |
|||
if n != 1: |
|||
return languages.get(language, {}).get(p, p) |
|||
return languages.get(language, {}).get(s, s) |
|||
|
|||
|
|||
i18n_env = Environment( |
|||
loader=DictLoader(i18n_templates), |
|||
extensions=['jinja2.ext.i18n'] |
|||
) |
|||
i18n_env.globals.update({ |
|||
'_': gettext, |
|||
'gettext': gettext, |
|||
'ngettext': ngettext |
|||
}) |
|||
|
|||
newstyle_i18n_env = Environment( |
|||
loader=DictLoader(newstyle_i18n_templates), |
|||
extensions=['jinja2.ext.i18n'] |
|||
) |
|||
newstyle_i18n_env.install_gettext_callables(gettext, ngettext, newstyle=True) |
|||
|
|||
class TestExtension(Extension): |
|||
tags = set(['test']) |
|||
ext_attr = 42 |
|||
|
|||
def parse(self, parser): |
|||
return nodes.Output([self.call_method('_dump', [ |
|||
nodes.EnvironmentAttribute('sandboxed'), |
|||
self.attr('ext_attr'), |
|||
nodes.ImportedName(__name__ + '.importable_object'), |
|||
nodes.ContextReference() |
|||
])]).set_lineno(next(parser.stream).lineno) |
|||
|
|||
def _dump(self, sandboxed, ext_attr, imported_object, context): |
|||
return '%s|%s|%s|%s' % ( |
|||
sandboxed, |
|||
ext_attr, |
|||
imported_object, |
|||
context.blocks |
|||
) |
|||
|
|||
|
|||
class PreprocessorExtension(Extension): |
|||
|
|||
def preprocess(self, source, name, filename=None): |
|||
return source.replace('[[TEST]]', '({{ foo }})') |
|||
|
|||
|
|||
class StreamFilterExtension(Extension): |
|||
|
|||
def filter_stream(self, stream): |
|||
for token in stream: |
|||
if token.type == 'data': |
|||
for t in self.interpolate(token): |
|||
yield t |
|||
else: |
|||
yield token |
|||
|
|||
def interpolate(self, token): |
|||
pos = 0 |
|||
end = len(token.value) |
|||
lineno = token.lineno |
|||
while 1: |
|||
match = _gettext_re.search(token.value, pos) |
|||
if match is None: |
|||
break |
|||
value = token.value[pos:match.start()] |
|||
if value: |
|||
yield Token(lineno, 'data', value) |
|||
lineno += count_newlines(token.value) |
|||
yield Token(lineno, 'variable_begin', None) |
|||
yield Token(lineno, 'name', 'gettext') |
|||
yield Token(lineno, 'lparen', None) |
|||
yield Token(lineno, 'string', match.group(1)) |
|||
yield Token(lineno, 'rparen', None) |
|||
yield Token(lineno, 'variable_end', None) |
|||
pos = match.end() |
|||
if pos < end: |
|||
yield Token(lineno, 'data', token.value[pos:]) |
|||
|
|||
|
|||
class ExtensionsTestCase(JinjaTestCase): |
|||
|
|||
def test_extend_late(self): |
|||
env = Environment() |
|||
env.add_extension('jinja2.ext.autoescape') |
|||
t = env.from_string('{% autoescape true %}{{ "<test>" }}{% endautoescape %}') |
|||
assert t.render() == '<test>' |
|||
|
|||
def test_loop_controls(self): |
|||
env = Environment(extensions=['jinja2.ext.loopcontrols']) |
|||
|
|||
tmpl = env.from_string(''' |
|||
{%- for item in [1, 2, 3, 4] %} |
|||
{%- if item % 2 == 0 %}{% continue %}{% endif -%} |
|||
{{ item }} |
|||
{%- endfor %}''') |
|||
assert tmpl.render() == '13' |
|||
|
|||
tmpl = env.from_string(''' |
|||
{%- for item in [1, 2, 3, 4] %} |
|||
{%- if item > 2 %}{% break %}{% endif -%} |
|||
{{ item }} |
|||
{%- endfor %}''') |
|||
assert tmpl.render() == '12' |
|||
|
|||
def test_do(self): |
|||
env = Environment(extensions=['jinja2.ext.do']) |
|||
tmpl = env.from_string(''' |
|||
{%- set items = [] %} |
|||
{%- for char in "foo" %} |
|||
{%- do items.append(loop.index0 ~ char) %} |
|||
{%- endfor %}{{ items|join(', ') }}''') |
|||
assert tmpl.render() == '0f, 1o, 2o' |
|||
|
|||
def test_with(self): |
|||
env = Environment(extensions=['jinja2.ext.with_']) |
|||
tmpl = env.from_string('''\ |
|||
{% with a=42, b=23 -%} |
|||
{{ a }} = {{ b }} |
|||
{% endwith -%} |
|||
{{ a }} = {{ b }}\ |
|||
''') |
|||
assert [x.strip() for x in tmpl.render(a=1, b=2).splitlines()] \ |
|||
== ['42 = 23', '1 = 2'] |
|||
|
|||
def test_extension_nodes(self): |
|||
env = Environment(extensions=[TestExtension]) |
|||
tmpl = env.from_string('{% test %}') |
|||
assert tmpl.render() == 'False|42|23|{}' |
|||
|
|||
def test_identifier(self): |
|||
assert TestExtension.identifier == __name__ + '.TestExtension' |
|||
|
|||
def test_rebinding(self): |
|||
original = Environment(extensions=[TestExtension]) |
|||
overlay = original.overlay() |
|||
for env in original, overlay: |
|||
for ext in env.extensions.itervalues(): |
|||
assert ext.environment is env |
|||
|
|||
def test_preprocessor_extension(self): |
|||
env = Environment(extensions=[PreprocessorExtension]) |
|||
tmpl = env.from_string('{[[TEST]]}') |
|||
assert tmpl.render(foo=42) == '{(42)}' |
|||
|
|||
def test_streamfilter_extension(self): |
|||
env = Environment(extensions=[StreamFilterExtension]) |
|||
env.globals['gettext'] = lambda x: x.upper() |
|||
tmpl = env.from_string('Foo _(bar) Baz') |
|||
out = tmpl.render() |
|||
assert out == 'Foo BAR Baz' |
|||
|
|||
def test_extension_ordering(self): |
|||
class T1(Extension): |
|||
priority = 1 |
|||
class T2(Extension): |
|||
priority = 2 |
|||
env = Environment(extensions=[T1, T2]) |
|||
ext = list(env.iter_extensions()) |
|||
assert ext[0].__class__ is T1 |
|||
assert ext[1].__class__ is T2 |
|||
|
|||
|
|||
class InternationalizationTestCase(JinjaTestCase): |
|||
|
|||
def test_trans(self): |
|||
tmpl = i18n_env.get_template('child.html') |
|||
assert tmpl.render(LANGUAGE='de') == '<title>fehlend</title>pass auf' |
|||
|
|||
def test_trans_plural(self): |
|||
tmpl = i18n_env.get_template('plural.html') |
|||
assert tmpl.render(LANGUAGE='de', user_count=1) == 'Ein Benutzer online' |
|||
assert tmpl.render(LANGUAGE='de', user_count=2) == '2 Benutzer online' |
|||
|
|||
def test_complex_plural(self): |
|||
tmpl = i18n_env.from_string('{% trans foo=42, count=2 %}{{ count }} item{% ' |
|||
'pluralize count %}{{ count }} items{% endtrans %}') |
|||
assert tmpl.render() == '2 items' |
|||
self.assert_raises(TemplateAssertionError, i18n_env.from_string, |
|||
'{% trans foo %}...{% pluralize bar %}...{% endtrans %}') |
|||
|
|||
def test_trans_stringformatting(self): |
|||
tmpl = i18n_env.get_template('stringformat.html') |
|||
assert tmpl.render(LANGUAGE='de', user_count=5) == 'Benutzer: 5' |
|||
|
|||
def test_extract(self): |
|||
from jinja2.ext import babel_extract |
|||
source = BytesIO(''' |
|||
{{ gettext('Hello World') }} |
|||
{% trans %}Hello World{% endtrans %} |
|||
{% trans %}{{ users }} user{% pluralize %}{{ users }} users{% endtrans %} |
|||
'''.encode('ascii')) # make python 3 happy |
|||
assert list(babel_extract(source, ('gettext', 'ngettext', '_'), [], {})) == [ |
|||
(2, 'gettext', u'Hello World', []), |
|||
(3, 'gettext', u'Hello World', []), |
|||
(4, 'ngettext', (u'%(users)s user', u'%(users)s users', None), []) |
|||
] |
|||
|
|||
def test_comment_extract(self): |
|||
from jinja2.ext import babel_extract |
|||
source = BytesIO(''' |
|||
{# trans first #} |
|||
{{ gettext('Hello World') }} |
|||
{% trans %}Hello World{% endtrans %}{# trans second #} |
|||
{#: third #} |
|||
{% trans %}{{ users }} user{% pluralize %}{{ users }} users{% endtrans %} |
|||
'''.encode('utf-8')) # make python 3 happy |
|||
assert list(babel_extract(source, ('gettext', 'ngettext', '_'), ['trans', ':'], {})) == [ |
|||
(3, 'gettext', u'Hello World', ['first']), |
|||
(4, 'gettext', u'Hello World', ['second']), |
|||
(6, 'ngettext', (u'%(users)s user', u'%(users)s users', None), ['third']) |
|||
] |
|||
|
|||
|
|||
class NewstyleInternationalizationTestCase(JinjaTestCase): |
|||
|
|||
def test_trans(self): |
|||
tmpl = newstyle_i18n_env.get_template('child.html') |
|||
assert tmpl.render(LANGUAGE='de') == '<title>fehlend</title>pass auf' |
|||
|
|||
def test_trans_plural(self): |
|||
tmpl = newstyle_i18n_env.get_template('plural.html') |
|||
assert tmpl.render(LANGUAGE='de', user_count=1) == 'Ein Benutzer online' |
|||
assert tmpl.render(LANGUAGE='de', user_count=2) == '2 Benutzer online' |
|||
|
|||
def test_complex_plural(self): |
|||
tmpl = newstyle_i18n_env.from_string('{% trans foo=42, count=2 %}{{ count }} item{% ' |
|||
'pluralize count %}{{ count }} items{% endtrans %}') |
|||
assert tmpl.render() == '2 items' |
|||
self.assert_raises(TemplateAssertionError, i18n_env.from_string, |
|||
'{% trans foo %}...{% pluralize bar %}...{% endtrans %}') |
|||
|
|||
def test_trans_stringformatting(self): |
|||
tmpl = newstyle_i18n_env.get_template('stringformat.html') |
|||
assert tmpl.render(LANGUAGE='de', user_count=5) == 'Benutzer: 5' |
|||
|
|||
def test_newstyle_plural(self): |
|||
tmpl = newstyle_i18n_env.get_template('ngettext.html') |
|||
assert tmpl.render(LANGUAGE='de', apples=1) == '1 Apfel' |
|||
assert tmpl.render(LANGUAGE='de', apples=5) == u'5 Äpfel' |
|||
|
|||
def test_autoescape_support(self): |
|||
env = Environment(extensions=['jinja2.ext.autoescape', |
|||
'jinja2.ext.i18n']) |
|||
env.install_gettext_callables(lambda x: u'<strong>Wert: %(name)s</strong>', |
|||
lambda s, p, n: s, newstyle=True) |
|||
t = env.from_string('{% autoescape ae %}{{ gettext("foo", name=' |
|||
'"<test>") }}{% endautoescape %}') |
|||
assert t.render(ae=True) == '<strong>Wert: <test></strong>' |
|||
assert t.render(ae=False) == '<strong>Wert: <test></strong>' |
|||
|
|||
def test_num_used_twice(self): |
|||
tmpl = newstyle_i18n_env.get_template('ngettext_long.html') |
|||
assert tmpl.render(apples=5, LANGUAGE='de') == u'5 Äpfel' |
|||
|
|||
def test_num_called_num(self): |
|||
source = newstyle_i18n_env.compile(''' |
|||
{% trans num=3 %}{{ num }} apple{% pluralize |
|||
%}{{ num }} apples{% endtrans %} |
|||
''', raw=True) |
|||
# quite hacky, but the only way to properly test that. The idea is |
|||
# that the generated code does not pass num twice (although that |
|||
# would work) for better performance. This only works on the |
|||
# newstyle gettext of course |
|||
assert re.search(r"l_ngettext, u?'\%\(num\)s apple', u?'\%\(num\)s " |
|||
r"apples', 3", source) is not None |
|||
|
|||
def test_trans_vars(self): |
|||
t1 = newstyle_i18n_env.get_template('transvars1.html') |
|||
t2 = newstyle_i18n_env.get_template('transvars2.html') |
|||
t3 = newstyle_i18n_env.get_template('transvars3.html') |
|||
assert t1.render(num=1, LANGUAGE='de') == 'Benutzer: 1' |
|||
assert t2.render(count=23, LANGUAGE='de') == 'Benutzer: 23' |
|||
assert t3.render(num=42, LANGUAGE='de') == 'Benutzer: 42' |
|||
|
|||
def test_novars_vars_escaping(self): |
|||
t = newstyle_i18n_env.get_template('novars.html') |
|||
assert t.render() == '%(hello)s' |
|||
t = newstyle_i18n_env.get_template('vars.html') |
|||
assert t.render(foo='42') == '42%(foo)s' |
|||
t = newstyle_i18n_env.get_template('explicitvars.html') |
|||
assert t.render() == '%(foo)s' |
|||
|
|||
|
|||
class AutoEscapeTestCase(JinjaTestCase): |
|||
|
|||
def test_scoped_setting(self): |
|||
env = Environment(extensions=['jinja2.ext.autoescape'], |
|||
autoescape=True) |
|||
tmpl = env.from_string(''' |
|||
{{ "<HelloWorld>" }} |
|||
{% autoescape false %} |
|||
{{ "<HelloWorld>" }} |
|||
{% endautoescape %} |
|||
{{ "<HelloWorld>" }} |
|||
''') |
|||
assert tmpl.render().split() == \ |
|||
[u'<HelloWorld>', u'<HelloWorld>', u'<HelloWorld>'] |
|||
|
|||
env = Environment(extensions=['jinja2.ext.autoescape'], |
|||
autoescape=False) |
|||
tmpl = env.from_string(''' |
|||
{{ "<HelloWorld>" }} |
|||
{% autoescape true %} |
|||
{{ "<HelloWorld>" }} |
|||
{% endautoescape %} |
|||
{{ "<HelloWorld>" }} |
|||
''') |
|||
assert tmpl.render().split() == \ |
|||
[u'<HelloWorld>', u'<HelloWorld>', u'<HelloWorld>'] |
|||
|
|||
def test_nonvolatile(self): |
|||
env = Environment(extensions=['jinja2.ext.autoescape'], |
|||
autoescape=True) |
|||
tmpl = env.from_string('{{ {"foo": "<test>"}|xmlattr|escape }}') |
|||
assert tmpl.render() == ' foo="<test>"' |
|||
tmpl = env.from_string('{% autoescape false %}{{ {"foo": "<test>"}' |
|||
'|xmlattr|escape }}{% endautoescape %}') |
|||
assert tmpl.render() == ' foo="&lt;test&gt;"' |
|||
|
|||
def test_volatile(self): |
|||
env = Environment(extensions=['jinja2.ext.autoescape'], |
|||
autoescape=True) |
|||
tmpl = env.from_string('{% autoescape foo %}{{ {"foo": "<test>"}' |
|||
'|xmlattr|escape }}{% endautoescape %}') |
|||
assert tmpl.render(foo=False) == ' foo="&lt;test&gt;"' |
|||
assert tmpl.render(foo=True) == ' foo="<test>"' |
|||
|
|||
def test_scoping(self): |
|||
env = Environment(extensions=['jinja2.ext.autoescape']) |
|||
tmpl = env.from_string('{% autoescape true %}{% set x = "<x>" %}{{ x }}' |
|||
'{% endautoescape %}{{ x }}{{ "<y>" }}') |
|||
assert tmpl.render(x=1) == '<x>1<y>' |
|||
|
|||
def test_volatile_scoping(self): |
|||
env = Environment(extensions=['jinja2.ext.autoescape']) |
|||
tmplsource = ''' |
|||
{% autoescape val %} |
|||
{% macro foo(x) %} |
|||
[{{ x }}] |
|||
{% endmacro %} |
|||
{{ foo().__class__.__name__ }} |
|||
{% endautoescape %} |
|||
{{ '<testing>' }} |
|||
''' |
|||
tmpl = env.from_string(tmplsource) |
|||
assert tmpl.render(val=True).split()[0] == 'Markup' |
|||
assert tmpl.render(val=False).split()[0] == unicode.__name__ |
|||
|
|||
# looking at the source we should see <testing> there in raw |
|||
# (and then escaped as well) |
|||
env = Environment(extensions=['jinja2.ext.autoescape']) |
|||
pysource = env.compile(tmplsource, raw=True) |
|||
assert '<testing>\\n' in pysource |
|||
|
|||
env = Environment(extensions=['jinja2.ext.autoescape'], |
|||
autoescape=True) |
|||
pysource = env.compile(tmplsource, raw=True) |
|||
assert '<testing>\\n' in pysource |
|||
|
|||
|
|||
def suite(): |
|||
suite = unittest.TestSuite() |
|||
suite.addTest(unittest.makeSuite(ExtensionsTestCase)) |
|||
suite.addTest(unittest.makeSuite(InternationalizationTestCase)) |
|||
suite.addTest(unittest.makeSuite(NewstyleInternationalizationTestCase)) |
|||
suite.addTest(unittest.makeSuite(AutoEscapeTestCase)) |
|||
return suite |
@ -1,356 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
jinja2.testsuite.filters |
|||
~~~~~~~~~~~~~~~~~~~~~~~~ |
|||
|
|||
Tests for the jinja filters. |
|||
|
|||
:copyright: (c) 2010 by the Jinja Team. |
|||
:license: BSD, see LICENSE for more details. |
|||
""" |
|||
import unittest |
|||
from jinja2.testsuite import JinjaTestCase |
|||
|
|||
from jinja2 import Markup, Environment |
|||
|
|||
env = Environment() |
|||
|
|||
|
|||
class FilterTestCase(JinjaTestCase): |
|||
|
|||
def test_capitalize(self): |
|||
tmpl = env.from_string('{{ "foo bar"|capitalize }}') |
|||
assert tmpl.render() == 'Foo bar' |
|||
|
|||
def test_center(self): |
|||
tmpl = env.from_string('{{ "foo"|center(9) }}') |
|||
assert tmpl.render() == ' foo ' |
|||
|
|||
def test_default(self): |
|||
tmpl = env.from_string( |
|||
"{{ missing|default('no') }}|{{ false|default('no') }}|" |
|||
"{{ false|default('no', true) }}|{{ given|default('no') }}" |
|||
) |
|||
assert tmpl.render(given='yes') == 'no|False|no|yes' |
|||
|
|||
def test_dictsort(self): |
|||
tmpl = env.from_string( |
|||
'{{ foo|dictsort }}|' |
|||
'{{ foo|dictsort(true) }}|' |
|||
'{{ foo|dictsort(false, "value") }}' |
|||
) |
|||
out = tmpl.render(foo={"aa": 0, "b": 1, "c": 2, "AB": 3}) |
|||
assert out == ("[('aa', 0), ('AB', 3), ('b', 1), ('c', 2)]|" |
|||
"[('AB', 3), ('aa', 0), ('b', 1), ('c', 2)]|" |
|||
"[('aa', 0), ('b', 1), ('c', 2), ('AB', 3)]") |
|||
|
|||
def test_batch(self): |
|||
tmpl = env.from_string("{{ foo|batch(3)|list }}|" |
|||
"{{ foo|batch(3, 'X')|list }}") |
|||
out = tmpl.render(foo=range(10)) |
|||
assert out == ("[[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]|" |
|||
"[[0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 'X', 'X']]") |
|||
|
|||
def test_slice(self): |
|||
tmpl = env.from_string('{{ foo|slice(3)|list }}|' |
|||
'{{ foo|slice(3, "X")|list }}') |
|||
out = tmpl.render(foo=range(10)) |
|||
assert out == ("[[0, 1, 2, 3], [4, 5, 6], [7, 8, 9]]|" |
|||
"[[0, 1, 2, 3], [4, 5, 6, 'X'], [7, 8, 9, 'X']]") |
|||
|
|||
def test_escape(self): |
|||
tmpl = env.from_string('''{{ '<">&'|escape }}''') |
|||
out = tmpl.render() |
|||
assert out == '<">&' |
|||
|
|||
def test_striptags(self): |
|||
tmpl = env.from_string('''{{ foo|striptags }}''') |
|||
out = tmpl.render(foo=' <p>just a small \n <a href="#">' |
|||
'example</a> link</p>\n<p>to a webpage</p> ' |
|||
'<!-- <p>and some commented stuff</p> -->') |
|||
assert out == 'just a small example link to a webpage' |
|||
|
|||
def test_filesizeformat(self): |
|||
tmpl = env.from_string( |
|||
'{{ 100|filesizeformat }}|' |
|||
'{{ 1000|filesizeformat }}|' |
|||
'{{ 1000000|filesizeformat }}|' |
|||
'{{ 1000000000|filesizeformat }}|' |
|||
'{{ 1000000000000|filesizeformat }}|' |
|||
'{{ 100|filesizeformat(true) }}|' |
|||
'{{ 1000|filesizeformat(true) }}|' |
|||
'{{ 1000000|filesizeformat(true) }}|' |
|||
'{{ 1000000000|filesizeformat(true) }}|' |
|||
'{{ 1000000000000|filesizeformat(true) }}' |
|||
) |
|||
out = tmpl.render() |
|||
assert out == ( |
|||
'100 Bytes|0.0 kB|0.0 MB|0.0 GB|0.0 TB|100 Bytes|' |
|||
'1000 Bytes|1.0 KiB|0.9 MiB|0.9 GiB' |
|||
) |
|||
|
|||
def test_first(self): |
|||
tmpl = env.from_string('{{ foo|first }}') |
|||
out = tmpl.render(foo=range(10)) |
|||
assert out == '0' |
|||
|
|||
def test_float(self): |
|||
tmpl = env.from_string('{{ "42"|float }}|' |
|||
'{{ "ajsghasjgd"|float }}|' |
|||
'{{ "32.32"|float }}') |
|||
out = tmpl.render() |
|||
assert out == '42.0|0.0|32.32' |
|||
|
|||
def test_format(self): |
|||
tmpl = env.from_string('''{{ "%s|%s"|format("a", "b") }}''') |
|||
out = tmpl.render() |
|||
assert out == 'a|b' |
|||
|
|||
def test_indent(self): |
|||
tmpl = env.from_string('{{ foo|indent(2) }}|{{ foo|indent(2, true) }}') |
|||
text = '\n'.join([' '.join(['foo', 'bar'] * 2)] * 2) |
|||
out = tmpl.render(foo=text) |
|||
assert out == ('foo bar foo bar\n foo bar foo bar| ' |
|||
'foo bar foo bar\n foo bar foo bar') |
|||
|
|||
def test_int(self): |
|||
tmpl = env.from_string('{{ "42"|int }}|{{ "ajsghasjgd"|int }}|' |
|||
'{{ "32.32"|int }}') |
|||
out = tmpl.render() |
|||
assert out == '42|0|32' |
|||
|
|||
def test_join(self): |
|||
tmpl = env.from_string('{{ [1, 2, 3]|join("|") }}') |
|||
out = tmpl.render() |
|||
assert out == '1|2|3' |
|||
|
|||
env2 = Environment(autoescape=True) |
|||
tmpl = env2.from_string('{{ ["<foo>", "<span>foo</span>"|safe]|join }}') |
|||
assert tmpl.render() == '<foo><span>foo</span>' |
|||
|
|||
def test_join_attribute(self): |
|||
class User(object): |
|||
def __init__(self, username): |
|||
self.username = username |
|||
tmpl = env.from_string('''{{ users|join(', ', 'username') }}''') |
|||
assert tmpl.render(users=map(User, ['foo', 'bar'])) == 'foo, bar' |
|||
|
|||
def test_last(self): |
|||
tmpl = env.from_string('''{{ foo|last }}''') |
|||
out = tmpl.render(foo=range(10)) |
|||
assert out == '9' |
|||
|
|||
def test_length(self): |
|||
tmpl = env.from_string('''{{ "hello world"|length }}''') |
|||
out = tmpl.render() |
|||
assert out == '11' |
|||
|
|||
def test_lower(self): |
|||
tmpl = env.from_string('''{{ "FOO"|lower }}''') |
|||
out = tmpl.render() |
|||
assert out == 'foo' |
|||
|
|||
def test_pprint(self): |
|||
from pprint import pformat |
|||
tmpl = env.from_string('''{{ data|pprint }}''') |
|||
data = range(1000) |
|||
assert tmpl.render(data=data) == pformat(data) |
|||
|
|||
def test_random(self): |
|||
tmpl = env.from_string('''{{ seq|random }}''') |
|||
seq = range(100) |
|||
for _ in range(10): |
|||
assert int(tmpl.render(seq=seq)) in seq |
|||
|
|||
def test_reverse(self): |
|||
tmpl = env.from_string('{{ "foobar"|reverse|join }}|' |
|||
'{{ [1, 2, 3]|reverse|list }}') |
|||
assert tmpl.render() == 'raboof|[3, 2, 1]' |
|||
|
|||
def test_string(self): |
|||
x = [1, 2, 3, 4, 5] |
|||
tmpl = env.from_string('''{{ obj|string }}''') |
|||
assert tmpl.render(obj=x) == unicode(x) |
|||
|
|||
def test_title(self): |
|||
tmpl = env.from_string('''{{ "foo bar"|title }}''') |
|||
assert tmpl.render() == "Foo Bar" |
|||
|
|||
def test_truncate(self): |
|||
tmpl = env.from_string( |
|||
'{{ data|truncate(15, true, ">>>") }}|' |
|||
'{{ data|truncate(15, false, ">>>") }}|' |
|||
'{{ smalldata|truncate(15) }}' |
|||
) |
|||
out = tmpl.render(data='foobar baz bar' * 1000, |
|||
smalldata='foobar baz bar') |
|||
assert out == 'foobar baz barf>>>|foobar baz >>>|foobar baz bar' |
|||
|
|||
def test_upper(self): |
|||
tmpl = env.from_string('{{ "foo"|upper }}') |
|||
assert tmpl.render() == 'FOO' |
|||
|
|||
def test_urlize(self): |
|||
tmpl = env.from_string('{{ "foo http://www.example.com/ bar"|urlize }}') |
|||
assert tmpl.render() == 'foo <a href="http://www.example.com/">'\ |
|||
'http://www.example.com/</a> bar' |
|||
|
|||
def test_wordcount(self): |
|||
tmpl = env.from_string('{{ "foo bar baz"|wordcount }}') |
|||
assert tmpl.render() == '3' |
|||
|
|||
def test_block(self): |
|||
tmpl = env.from_string('{% filter lower|escape %}<HEHE>{% endfilter %}') |
|||
assert tmpl.render() == '<hehe>' |
|||
|
|||
def test_chaining(self): |
|||
tmpl = env.from_string('''{{ ['<foo>', '<bar>']|first|upper|escape }}''') |
|||
assert tmpl.render() == '<FOO>' |
|||
|
|||
def test_sum(self): |
|||
tmpl = env.from_string('''{{ [1, 2, 3, 4, 5, 6]|sum }}''') |
|||
assert tmpl.render() == '21' |
|||
|
|||
def test_sum_attributes(self): |
|||
tmpl = env.from_string('''{{ values|sum('value') }}''') |
|||
assert tmpl.render(values=[ |
|||
{'value': 23}, |
|||
{'value': 1}, |
|||
{'value': 18}, |
|||
]) == '42' |
|||
|
|||
def test_sum_attributes_nested(self): |
|||
tmpl = env.from_string('''{{ values|sum('real.value') }}''') |
|||
assert tmpl.render(values=[ |
|||
{'real': {'value': 23}}, |
|||
{'real': {'value': 1}}, |
|||
{'real': {'value': 18}}, |
|||
]) == '42' |
|||
|
|||
def test_abs(self): |
|||
tmpl = env.from_string('''{{ -1|abs }}|{{ 1|abs }}''') |
|||
assert tmpl.render() == '1|1', tmpl.render() |
|||
|
|||
def test_round_positive(self): |
|||
tmpl = env.from_string('{{ 2.7|round }}|{{ 2.1|round }}|' |
|||
"{{ 2.1234|round(3, 'floor') }}|" |
|||
"{{ 2.1|round(0, 'ceil') }}") |
|||
assert tmpl.render() == '3.0|2.0|2.123|3.0', tmpl.render() |
|||
|
|||
def test_round_negative(self): |
|||
tmpl = env.from_string('{{ 21.3|round(-1)}}|' |
|||
"{{ 21.3|round(-1, 'ceil')}}|" |
|||
"{{ 21.3|round(-1, 'floor')}}") |
|||
assert tmpl.render() == '20.0|30.0|20.0',tmpl.render() |
|||
|
|||
def test_xmlattr(self): |
|||
tmpl = env.from_string("{{ {'foo': 42, 'bar': 23, 'fish': none, " |
|||
"'spam': missing, 'blub:blub': '<?>'}|xmlattr }}") |
|||
out = tmpl.render().split() |
|||
assert len(out) == 3 |
|||
assert 'foo="42"' in out |
|||
assert 'bar="23"' in out |
|||
assert 'blub:blub="<?>"' in out |
|||
|
|||
def test_sort1(self): |
|||
tmpl = env.from_string('{{ [2, 3, 1]|sort }}|{{ [2, 3, 1]|sort(true) }}') |
|||
assert tmpl.render() == '[1, 2, 3]|[3, 2, 1]' |
|||
|
|||
def test_sort2(self): |
|||
tmpl = env.from_string('{{ "".join(["c", "A", "b", "D"]|sort) }}') |
|||
assert tmpl.render() == 'AbcD' |
|||
|
|||
def test_sort3(self): |
|||
tmpl = env.from_string('''{{ ['foo', 'Bar', 'blah']|sort }}''') |
|||
assert tmpl.render() == "['Bar', 'blah', 'foo']" |
|||
|
|||
def test_sort4(self): |
|||
class Magic(object): |
|||
def __init__(self, value): |
|||
self.value = value |
|||
def __unicode__(self): |
|||
return unicode(self.value) |
|||
tmpl = env.from_string('''{{ items|sort(attribute='value')|join }}''') |
|||
assert tmpl.render(items=map(Magic, [3, 2, 4, 1])) == '1234' |
|||
|
|||
def test_groupby(self): |
|||
tmpl = env.from_string(''' |
|||
{%- for grouper, list in [{'foo': 1, 'bar': 2}, |
|||
{'foo': 2, 'bar': 3}, |
|||
{'foo': 1, 'bar': 1}, |
|||
{'foo': 3, 'bar': 4}]|groupby('foo') -%} |
|||
{{ grouper }}{% for x in list %}: {{ x.foo }}, {{ x.bar }}{% endfor %}| |
|||
{%- endfor %}''') |
|||
assert tmpl.render().split('|') == [ |
|||
"1: 1, 2: 1, 1", |
|||
"2: 2, 3", |
|||
"3: 3, 4", |
|||
"" |
|||
] |
|||
|
|||
def test_groupby_tuple_index(self): |
|||
tmpl = env.from_string(''' |
|||
{%- for grouper, list in [('a', 1), ('a', 2), ('b', 1)]|groupby(0) -%} |
|||
{{ grouper }}{% for x in list %}:{{ x.1 }}{% endfor %}| |
|||
{%- endfor %}''') |
|||
assert tmpl.render() == 'a:1:2|b:1|' |
|||
|
|||
def test_groupby_multidot(self): |
|||
class Date(object): |
|||
def __init__(self, day, month, year): |
|||
self.day = day |
|||
self.month = month |
|||
self.year = year |
|||
class Article(object): |
|||
def __init__(self, title, *date): |
|||
self.date = Date(*date) |
|||
self.title = title |
|||
articles = [ |
|||
Article('aha', 1, 1, 1970), |
|||
Article('interesting', 2, 1, 1970), |
|||
Article('really?', 3, 1, 1970), |
|||
Article('totally not', 1, 1, 1971) |
|||
] |
|||
tmpl = env.from_string(''' |
|||
{%- for year, list in articles|groupby('date.year') -%} |
|||
{{ year }}{% for x in list %}[{{ x.title }}]{% endfor %}| |
|||
{%- endfor %}''') |
|||
assert tmpl.render(articles=articles).split('|') == [ |
|||
'1970[aha][interesting][really?]', |
|||
'1971[totally not]', |
|||
'' |
|||
] |
|||
|
|||
def test_filtertag(self): |
|||
tmpl = env.from_string("{% filter upper|replace('FOO', 'foo') %}" |
|||
"foobar{% endfilter %}") |
|||
assert tmpl.render() == 'fooBAR' |
|||
|
|||
def test_replace(self): |
|||
env = Environment() |
|||
tmpl = env.from_string('{{ string|replace("o", 42) }}') |
|||
assert tmpl.render(string='<foo>') == '<f4242>' |
|||
env = Environment(autoescape=True) |
|||
tmpl = env.from_string('{{ string|replace("o", 42) }}') |
|||
assert tmpl.render(string='<foo>') == '<f4242>' |
|||
tmpl = env.from_string('{{ string|replace("<", 42) }}') |
|||
assert tmpl.render(string='<foo>') == '42foo>' |
|||
tmpl = env.from_string('{{ string|replace("o", ">x<") }}') |
|||
assert tmpl.render(string=Markup('foo')) == 'f>x<>x<' |
|||
|
|||
def test_forceescape(self): |
|||
tmpl = env.from_string('{{ x|forceescape }}') |
|||
assert tmpl.render(x=Markup('<div />')) == u'<div />' |
|||
|
|||
def test_safe(self): |
|||
env = Environment(autoescape=True) |
|||
tmpl = env.from_string('{{ "<div>foo</div>"|safe }}') |
|||
assert tmpl.render() == '<div>foo</div>' |
|||
tmpl = env.from_string('{{ "<div>foo</div>" }}') |
|||
assert tmpl.render() == '<div>foo</div>' |
|||
|
|||
|
|||
def suite(): |
|||
suite = unittest.TestSuite() |
|||
suite.addTest(unittest.makeSuite(FilterTestCase)) |
|||
return suite |
@ -1,141 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
jinja2.testsuite.imports |
|||
~~~~~~~~~~~~~~~~~~~~~~~~ |
|||
|
|||
Tests the import features (with includes). |
|||
|
|||
:copyright: (c) 2010 by the Jinja Team. |
|||
:license: BSD, see LICENSE for more details. |
|||
""" |
|||
import unittest |
|||
|
|||
from jinja2.testsuite import JinjaTestCase |
|||
|
|||
from jinja2 import Environment, DictLoader |
|||
from jinja2.exceptions import TemplateNotFound, TemplatesNotFound |
|||
|
|||
|
|||
test_env = Environment(loader=DictLoader(dict( |
|||
module='{% macro test() %}[{{ foo }}|{{ bar }}]{% endmacro %}', |
|||
header='[{{ foo }}|{{ 23 }}]', |
|||
o_printer='({{ o }})' |
|||
))) |
|||
test_env.globals['bar'] = 23 |
|||
|
|||
|
|||
class ImportsTestCase(JinjaTestCase): |
|||
|
|||
def test_context_imports(self): |
|||
t = test_env.from_string('{% import "module" as m %}{{ m.test() }}') |
|||
assert t.render(foo=42) == '[|23]' |
|||
t = test_env.from_string('{% import "module" as m without context %}{{ m.test() }}') |
|||
assert t.render(foo=42) == '[|23]' |
|||
t = test_env.from_string('{% import "module" as m with context %}{{ m.test() }}') |
|||
assert t.render(foo=42) == '[42|23]' |
|||
t = test_env.from_string('{% from "module" import test %}{{ test() }}') |
|||
assert t.render(foo=42) == '[|23]' |
|||
t = test_env.from_string('{% from "module" import test without context %}{{ test() }}') |
|||
assert t.render(foo=42) == '[|23]' |
|||
t = test_env.from_string('{% from "module" import test with context %}{{ test() }}') |
|||
assert t.render(foo=42) == '[42|23]' |
|||
|
|||
def test_trailing_comma(self): |
|||
test_env.from_string('{% from "foo" import bar, baz with context %}') |
|||
test_env.from_string('{% from "foo" import bar, baz, with context %}') |
|||
test_env.from_string('{% from "foo" import bar, with context %}') |
|||
test_env.from_string('{% from "foo" import bar, with, context %}') |
|||
test_env.from_string('{% from "foo" import bar, with with context %}') |
|||
|
|||
def test_exports(self): |
|||
m = test_env.from_string(''' |
|||
{% macro toplevel() %}...{% endmacro %} |
|||
{% macro __private() %}...{% endmacro %} |
|||
{% set variable = 42 %} |
|||
{% for item in [1] %} |
|||
{% macro notthere() %}{% endmacro %} |
|||
{% endfor %} |
|||
''').module |
|||
assert m.toplevel() == '...' |
|||
assert not hasattr(m, '__missing') |
|||
assert m.variable == 42 |
|||
assert not hasattr(m, 'notthere') |
|||
|
|||
|
|||
class IncludesTestCase(JinjaTestCase): |
|||
|
|||
def test_context_include(self): |
|||
t = test_env.from_string('{% include "header" %}') |
|||
assert t.render(foo=42) == '[42|23]' |
|||
t = test_env.from_string('{% include "header" with context %}') |
|||
assert t.render(foo=42) == '[42|23]' |
|||
t = test_env.from_string('{% include "header" without context %}') |
|||
assert t.render(foo=42) == '[|23]' |
|||
|
|||
def test_choice_includes(self): |
|||
t = test_env.from_string('{% include ["missing", "header"] %}') |
|||
assert t.render(foo=42) == '[42|23]' |
|||
|
|||
t = test_env.from_string('{% include ["missing", "missing2"] ignore missing %}') |
|||
assert t.render(foo=42) == '' |
|||
|
|||
t = test_env.from_string('{% include ["missing", "missing2"] %}') |
|||
self.assert_raises(TemplateNotFound, t.render) |
|||
try: |
|||
t.render() |
|||
except TemplatesNotFound, e: |
|||
assert e.templates == ['missing', 'missing2'] |
|||
assert e.name == 'missing2' |
|||
else: |
|||
assert False, 'thou shalt raise' |
|||
|
|||
def test_includes(t, **ctx): |
|||
ctx['foo'] = 42 |
|||
assert t.render(ctx) == '[42|23]' |
|||
|
|||
t = test_env.from_string('{% include ["missing", "header"] %}') |
|||
test_includes(t) |
|||
t = test_env.from_string('{% include x %}') |
|||
test_includes(t, x=['missing', 'header']) |
|||
t = test_env.from_string('{% include [x, "header"] %}') |
|||
test_includes(t, x='missing') |
|||
t = test_env.from_string('{% include x %}') |
|||
test_includes(t, x='header') |
|||
t = test_env.from_string('{% include x %}') |
|||
test_includes(t, x='header') |
|||
t = test_env.from_string('{% include [x] %}') |
|||
test_includes(t, x='header') |
|||
|
|||
def test_include_ignoring_missing(self): |
|||
t = test_env.from_string('{% include "missing" %}') |
|||
self.assert_raises(TemplateNotFound, t.render) |
|||
for extra in '', 'with context', 'without context': |
|||
t = test_env.from_string('{% include "missing" ignore missing ' + |
|||
extra + ' %}') |
|||
assert t.render() == '' |
|||
|
|||
def test_context_include_with_overrides(self): |
|||
env = Environment(loader=DictLoader(dict( |
|||
main="{% for item in [1, 2, 3] %}{% include 'item' %}{% endfor %}", |
|||
item="{{ item }}" |
|||
))) |
|||
assert env.get_template("main").render() == "123" |
|||
|
|||
def test_unoptimized_scopes(self): |
|||
t = test_env.from_string(""" |
|||
{% macro outer(o) %} |
|||
{% macro inner() %} |
|||
{% include "o_printer" %} |
|||
{% endmacro %} |
|||
{{ inner() }} |
|||
{% endmacro %} |
|||
{{ outer("FOO") }} |
|||
""") |
|||
assert t.render().strip() == '(FOO)' |
|||
|
|||
|
|||
def suite(): |
|||
suite = unittest.TestSuite() |
|||
suite.addTest(unittest.makeSuite(ImportsTestCase)) |
|||
suite.addTest(unittest.makeSuite(IncludesTestCase)) |
|||
return suite |
@ -1,227 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
jinja2.testsuite.inheritance |
|||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|||
|
|||
Tests the template inheritance feature. |
|||
|
|||
:copyright: (c) 2010 by the Jinja Team. |
|||
:license: BSD, see LICENSE for more details. |
|||
""" |
|||
import unittest |
|||
|
|||
from jinja2.testsuite import JinjaTestCase |
|||
|
|||
from jinja2 import Environment, DictLoader |
|||
|
|||
|
|||
LAYOUTTEMPLATE = '''\ |
|||
|{% block block1 %}block 1 from layout{% endblock %} |
|||
|{% block block2 %}block 2 from layout{% endblock %} |
|||
|{% block block3 %} |
|||
{% block block4 %}nested block 4 from layout{% endblock %} |
|||
{% endblock %}|''' |
|||
|
|||
LEVEL1TEMPLATE = '''\ |
|||
{% extends "layout" %} |
|||
{% block block1 %}block 1 from level1{% endblock %}''' |
|||
|
|||
LEVEL2TEMPLATE = '''\ |
|||
{% extends "level1" %} |
|||
{% block block2 %}{% block block5 %}nested block 5 from level2{% |
|||
endblock %}{% endblock %}''' |
|||
|
|||
LEVEL3TEMPLATE = '''\ |
|||
{% extends "level2" %} |
|||
{% block block5 %}block 5 from level3{% endblock %} |
|||
{% block block4 %}block 4 from level3{% endblock %} |
|||
''' |
|||
|
|||
LEVEL4TEMPLATE = '''\ |
|||
{% extends "level3" %} |
|||
{% block block3 %}block 3 from level4{% endblock %} |
|||
''' |
|||
|
|||
WORKINGTEMPLATE = '''\ |
|||
{% extends "layout" %} |
|||
{% block block1 %} |
|||
{% if false %} |
|||
{% block block2 %} |
|||
this should workd |
|||
{% endblock %} |
|||
{% endif %} |
|||
{% endblock %} |
|||
''' |
|||
|
|||
env = Environment(loader=DictLoader({ |
|||
'layout': LAYOUTTEMPLATE, |
|||
'level1': LEVEL1TEMPLATE, |
|||
'level2': LEVEL2TEMPLATE, |
|||
'level3': LEVEL3TEMPLATE, |
|||
'level4': LEVEL4TEMPLATE, |
|||
'working': WORKINGTEMPLATE |
|||
}), trim_blocks=True) |
|||
|
|||
|
|||
class InheritanceTestCase(JinjaTestCase): |
|||
|
|||
def test_layout(self): |
|||
tmpl = env.get_template('layout') |
|||
assert tmpl.render() == ('|block 1 from layout|block 2 from ' |
|||
'layout|nested block 4 from layout|') |
|||
|
|||
def test_level1(self): |
|||
tmpl = env.get_template('level1') |
|||
assert tmpl.render() == ('|block 1 from level1|block 2 from ' |
|||
'layout|nested block 4 from layout|') |
|||
|
|||
def test_level2(self): |
|||
tmpl = env.get_template('level2') |
|||
assert tmpl.render() == ('|block 1 from level1|nested block 5 from ' |
|||
'level2|nested block 4 from layout|') |
|||
|
|||
def test_level3(self): |
|||
tmpl = env.get_template('level3') |
|||
assert tmpl.render() == ('|block 1 from level1|block 5 from level3|' |
|||
'block 4 from level3|') |
|||
|
|||
def test_level4(sel): |
|||
tmpl = env.get_template('level4') |
|||
assert tmpl.render() == ('|block 1 from level1|block 5 from ' |
|||
'level3|block 3 from level4|') |
|||
|
|||
def test_super(self): |
|||
env = Environment(loader=DictLoader({ |
|||
'a': '{% block intro %}INTRO{% endblock %}|' |
|||
'BEFORE|{% block data %}INNER{% endblock %}|AFTER', |
|||
'b': '{% extends "a" %}{% block data %}({{ ' |
|||
'super() }}){% endblock %}', |
|||
'c': '{% extends "b" %}{% block intro %}--{{ ' |
|||
'super() }}--{% endblock %}\n{% block data ' |
|||
'%}[{{ super() }}]{% endblock %}' |
|||
})) |
|||
tmpl = env.get_template('c') |
|||
assert tmpl.render() == '--INTRO--|BEFORE|[(INNER)]|AFTER' |
|||
|
|||
def test_working(self): |
|||
tmpl = env.get_template('working') |
|||
|
|||
def test_reuse_blocks(self): |
|||
tmpl = env.from_string('{{ self.foo() }}|{% block foo %}42' |
|||
'{% endblock %}|{{ self.foo() }}') |
|||
assert tmpl.render() == '42|42|42' |
|||
|
|||
def test_preserve_blocks(self): |
|||
env = Environment(loader=DictLoader({ |
|||
'a': '{% if false %}{% block x %}A{% endblock %}{% endif %}{{ self.x() }}', |
|||
'b': '{% extends "a" %}{% block x %}B{{ super() }}{% endblock %}' |
|||
})) |
|||
tmpl = env.get_template('b') |
|||
assert tmpl.render() == 'BA' |
|||
|
|||
def test_dynamic_inheritance(self): |
|||
env = Environment(loader=DictLoader({ |
|||
'master1': 'MASTER1{% block x %}{% endblock %}', |
|||
'master2': 'MASTER2{% block x %}{% endblock %}', |
|||
'child': '{% extends master %}{% block x %}CHILD{% endblock %}' |
|||
})) |
|||
tmpl = env.get_template('child') |
|||
for m in range(1, 3): |
|||
assert tmpl.render(master='master%d' % m) == 'MASTER%dCHILD' % m |
|||
|
|||
def test_multi_inheritance(self): |
|||
env = Environment(loader=DictLoader({ |
|||
'master1': 'MASTER1{% block x %}{% endblock %}', |
|||
'master2': 'MASTER2{% block x %}{% endblock %}', |
|||
'child': '''{% if master %}{% extends master %}{% else %}{% extends |
|||
'master1' %}{% endif %}{% block x %}CHILD{% endblock %}''' |
|||
})) |
|||
tmpl = env.get_template('child') |
|||
assert tmpl.render(master='master2') == 'MASTER2CHILD' |
|||
assert tmpl.render(master='master1') == 'MASTER1CHILD' |
|||
assert tmpl.render() == 'MASTER1CHILD' |
|||
|
|||
def test_scoped_block(self): |
|||
env = Environment(loader=DictLoader({ |
|||
'master.html': '{% for item in seq %}[{% block item scoped %}' |
|||
'{% endblock %}]{% endfor %}' |
|||
})) |
|||
t = env.from_string('{% extends "master.html" %}{% block item %}' |
|||
'{{ item }}{% endblock %}') |
|||
assert t.render(seq=range(5)) == '[0][1][2][3][4]' |
|||
|
|||
def test_super_in_scoped_block(self): |
|||
env = Environment(loader=DictLoader({ |
|||
'master.html': '{% for item in seq %}[{% block item scoped %}' |
|||
'{{ item }}{% endblock %}]{% endfor %}' |
|||
})) |
|||
t = env.from_string('{% extends "master.html" %}{% block item %}' |
|||
'{{ super() }}|{{ item * 2 }}{% endblock %}') |
|||
assert t.render(seq=range(5)) == '[0|0][1|2][2|4][3|6][4|8]' |
|||
|
|||
def test_scoped_block_after_inheritance(self): |
|||
env = Environment(loader=DictLoader({ |
|||
'layout.html': ''' |
|||
{% block useless %}{% endblock %} |
|||
''', |
|||
'index.html': ''' |
|||
{%- extends 'layout.html' %} |
|||
{% from 'helpers.html' import foo with context %} |
|||
{% block useless %} |
|||
{% for x in [1, 2, 3] %} |
|||
{% block testing scoped %} |
|||
{{ foo(x) }} |
|||
{% endblock %} |
|||
{% endfor %} |
|||
{% endblock %} |
|||
''', |
|||
'helpers.html': ''' |
|||
{% macro foo(x) %}{{ the_foo + x }}{% endmacro %} |
|||
''' |
|||
})) |
|||
rv = env.get_template('index.html').render(the_foo=42).split() |
|||
assert rv == ['43', '44', '45'] |
|||
|
|||
|
|||
class BugFixTestCase(JinjaTestCase): |
|||
|
|||
def test_fixed_macro_scoping_bug(self): |
|||
assert Environment(loader=DictLoader({ |
|||
'test.html': '''\ |
|||
{% extends 'details.html' %} |
|||
|
|||
{% macro my_macro() %} |
|||
my_macro |
|||
{% endmacro %} |
|||
|
|||
{% block inner_box %} |
|||
{{ my_macro() }} |
|||
{% endblock %} |
|||
''', |
|||
'details.html': '''\ |
|||
{% extends 'standard.html' %} |
|||
|
|||
{% macro my_macro() %} |
|||
my_macro |
|||
{% endmacro %} |
|||
|
|||
{% block content %} |
|||
{% block outer_box %} |
|||
outer_box |
|||
{% block inner_box %} |
|||
inner_box |
|||
{% endblock %} |
|||
{% endblock %} |
|||
{% endblock %} |
|||
''', |
|||
'standard.html': ''' |
|||
{% block content %} {% endblock %} |
|||
''' |
|||
})).get_template("test.html").render().split() == [u'outer_box', u'my_macro'] |
|||
|
|||
|
|||
def suite(): |
|||
suite = unittest.TestSuite() |
|||
suite.addTest(unittest.makeSuite(InheritanceTestCase)) |
|||
suite.addTest(unittest.makeSuite(BugFixTestCase)) |
|||
return suite |
@ -1,387 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
jinja2.testsuite.lexnparse |
|||
~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|||
|
|||
All the unittests regarding lexing, parsing and syntax. |
|||
|
|||
:copyright: (c) 2010 by the Jinja Team. |
|||
:license: BSD, see LICENSE for more details. |
|||
""" |
|||
import sys |
|||
import unittest |
|||
|
|||
from jinja2.testsuite import JinjaTestCase |
|||
|
|||
from jinja2 import Environment, Template, TemplateSyntaxError, \ |
|||
UndefinedError, nodes |
|||
|
|||
env = Environment() |
|||
|
|||
|
|||
# how does a string look like in jinja syntax? |
|||
if sys.version_info < (3, 0): |
|||
def jinja_string_repr(string): |
|||
return repr(string)[1:] |
|||
else: |
|||
jinja_string_repr = repr |
|||
|
|||
|
|||
class LexerTestCase(JinjaTestCase): |
|||
|
|||
def test_raw1(self): |
|||
tmpl = env.from_string('{% raw %}foo{% endraw %}|' |
|||
'{%raw%}{{ bar }}|{% baz %}{% endraw %}') |
|||
assert tmpl.render() == 'foo|{{ bar }}|{% baz %}' |
|||
|
|||
def test_raw2(self): |
|||
tmpl = env.from_string('1 {%- raw -%} 2 {%- endraw -%} 3') |
|||
assert tmpl.render() == '123' |
|||
|
|||
def test_balancing(self): |
|||
env = Environment('{%', '%}', '${', '}') |
|||
tmpl = env.from_string('''{% for item in seq |
|||
%}${{'foo': item}|upper}{% endfor %}''') |
|||
assert tmpl.render(seq=range(3)) == "{'FOO': 0}{'FOO': 1}{'FOO': 2}" |
|||
|
|||
def test_comments(self): |
|||
env = Environment('<!--', '-->', '{', '}') |
|||
tmpl = env.from_string('''\ |
|||
<ul> |
|||
<!--- for item in seq --> |
|||
<li>{item}</li> |
|||
<!--- endfor --> |
|||
</ul>''') |
|||
assert tmpl.render(seq=range(3)) == ("<ul>\n <li>0</li>\n " |
|||
"<li>1</li>\n <li>2</li>\n</ul>") |
|||
|
|||
def test_string_escapes(self): |
|||
for char in u'\0', u'\u2668', u'\xe4', u'\t', u'\r', u'\n': |
|||
tmpl = env.from_string('{{ %s }}' % jinja_string_repr(char)) |
|||
assert tmpl.render() == char |
|||
assert env.from_string('{{ "\N{HOT SPRINGS}" }}').render() == u'\u2668' |
|||
|
|||
def test_bytefallback(self): |
|||
from pprint import pformat |
|||
tmpl = env.from_string(u'''{{ 'foo'|pprint }}|{{ 'bär'|pprint }}''') |
|||
assert tmpl.render() == pformat('foo') + '|' + pformat(u'bär') |
|||
|
|||
def test_operators(self): |
|||
from jinja2.lexer import operators |
|||
for test, expect in operators.iteritems(): |
|||
if test in '([{}])': |
|||
continue |
|||
stream = env.lexer.tokenize('{{ %s }}' % test) |
|||
stream.next() |
|||
assert stream.current.type == expect |
|||
|
|||
def test_normalizing(self): |
|||
for seq in '\r', '\r\n', '\n': |
|||
env = Environment(newline_sequence=seq) |
|||
tmpl = env.from_string('1\n2\r\n3\n4\n') |
|||
result = tmpl.render() |
|||
assert result.replace(seq, 'X') == '1X2X3X4' |
|||
|
|||
|
|||
class ParserTestCase(JinjaTestCase): |
|||
|
|||
def test_php_syntax(self): |
|||
env = Environment('<?', '?>', '<?=', '?>', '<!--', '-->') |
|||
tmpl = env.from_string('''\ |
|||
<!-- I'm a comment, I'm not interesting -->\ |
|||
<? for item in seq -?> |
|||
<?= item ?> |
|||
<?- endfor ?>''') |
|||
assert tmpl.render(seq=range(5)) == '01234' |
|||
|
|||
def test_erb_syntax(self): |
|||
env = Environment('<%', '%>', '<%=', '%>', '<%#', '%>') |
|||
tmpl = env.from_string('''\ |
|||
<%# I'm a comment, I'm not interesting %>\ |
|||
<% for item in seq -%> |
|||
<%= item %> |
|||
<%- endfor %>''') |
|||
assert tmpl.render(seq=range(5)) == '01234' |
|||
|
|||
def test_comment_syntax(self): |
|||
env = Environment('<!--', '-->', '${', '}', '<!--#', '-->') |
|||
tmpl = env.from_string('''\ |
|||
<!--# I'm a comment, I'm not interesting -->\ |
|||
<!-- for item in seq ---> |
|||
${item} |
|||
<!--- endfor -->''') |
|||
assert tmpl.render(seq=range(5)) == '01234' |
|||
|
|||
def test_balancing(self): |
|||
tmpl = env.from_string('''{{{'foo':'bar'}.foo}}''') |
|||
assert tmpl.render() == 'bar' |
|||
|
|||
def test_start_comment(self): |
|||
tmpl = env.from_string('''{# foo comment |
|||
and bar comment #} |
|||
{% macro blub() %}foo{% endmacro %} |
|||
{{ blub() }}''') |
|||
assert tmpl.render().strip() == 'foo' |
|||
|
|||
def test_line_syntax(self): |
|||
env = Environment('<%', '%>', '${', '}', '<%#', '%>', '%') |
|||
tmpl = env.from_string('''\ |
|||
<%# regular comment %> |
|||
% for item in seq: |
|||
${item} |
|||
% endfor''') |
|||
assert [int(x.strip()) for x in tmpl.render(seq=range(5)).split()] == \ |
|||
range(5) |
|||
|
|||
env = Environment('<%', '%>', '${', '}', '<%#', '%>', '%', '##') |
|||
tmpl = env.from_string('''\ |
|||
<%# regular comment %> |
|||
% for item in seq: |
|||
${item} ## the rest of the stuff |
|||
% endfor''') |
|||
assert [int(x.strip()) for x in tmpl.render(seq=range(5)).split()] == \ |
|||
range(5) |
|||
|
|||
def test_line_syntax_priority(self): |
|||
# XXX: why is the whitespace there in front of the newline? |
|||
env = Environment('{%', '%}', '${', '}', '/*', '*/', '##', '#') |
|||
tmpl = env.from_string('''\ |
|||
/* ignore me. |
|||
I'm a multiline comment */ |
|||
## for item in seq: |
|||
* ${item} # this is just extra stuff |
|||
## endfor''') |
|||
assert tmpl.render(seq=[1, 2]).strip() == '* 1\n* 2' |
|||
env = Environment('{%', '%}', '${', '}', '/*', '*/', '#', '##') |
|||
tmpl = env.from_string('''\ |
|||
/* ignore me. |
|||
I'm a multiline comment */ |
|||
# for item in seq: |
|||
* ${item} ## this is just extra stuff |
|||
## extra stuff i just want to ignore |
|||
# endfor''') |
|||
assert tmpl.render(seq=[1, 2]).strip() == '* 1\n\n* 2' |
|||
|
|||
def test_error_messages(self): |
|||
def assert_error(code, expected): |
|||
try: |
|||
Template(code) |
|||
except TemplateSyntaxError, e: |
|||
assert str(e) == expected, 'unexpected error message' |
|||
else: |
|||
assert False, 'that was suposed to be an error' |
|||
|
|||
assert_error('{% for item in seq %}...{% endif %}', |
|||
"Encountered unknown tag 'endif'. Jinja was looking " |
|||
"for the following tags: 'endfor' or 'else'. The " |
|||
"innermost block that needs to be closed is 'for'.") |
|||
assert_error('{% if foo %}{% for item in seq %}...{% endfor %}{% endfor %}', |
|||
"Encountered unknown tag 'endfor'. Jinja was looking for " |
|||
"the following tags: 'elif' or 'else' or 'endif'. The " |
|||
"innermost block that needs to be closed is 'if'.") |
|||
assert_error('{% if foo %}', |
|||
"Unexpected end of template. Jinja was looking for the " |
|||
"following tags: 'elif' or 'else' or 'endif'. The " |
|||
"innermost block that needs to be closed is 'if'.") |
|||
assert_error('{% for item in seq %}', |
|||
"Unexpected end of template. Jinja was looking for the " |
|||
"following tags: 'endfor' or 'else'. The innermost block " |
|||
"that needs to be closed is 'for'.") |
|||
assert_error('{% block foo-bar-baz %}', |
|||
"Block names in Jinja have to be valid Python identifiers " |
|||
"and may not contain hypens, use an underscore instead.") |
|||
assert_error('{% unknown_tag %}', |
|||
"Encountered unknown tag 'unknown_tag'.") |
|||
|
|||
|
|||
class SyntaxTestCase(JinjaTestCase): |
|||
|
|||
def test_call(self): |
|||
env = Environment() |
|||
env.globals['foo'] = lambda a, b, c, e, g: a + b + c + e + g |
|||
tmpl = env.from_string("{{ foo('a', c='d', e='f', *['b'], **{'g': 'h'}) }}") |
|||
assert tmpl.render() == 'abdfh' |
|||
|
|||
def test_slicing(self): |
|||
tmpl = env.from_string('{{ [1, 2, 3][:] }}|{{ [1, 2, 3][::-1] }}') |
|||
assert tmpl.render() == '[1, 2, 3]|[3, 2, 1]' |
|||
|
|||
def test_attr(self): |
|||
tmpl = env.from_string("{{ foo.bar }}|{{ foo['bar'] }}") |
|||
assert tmpl.render(foo={'bar': 42}) == '42|42' |
|||
|
|||
def test_subscript(self): |
|||
tmpl = env.from_string("{{ foo[0] }}|{{ foo[-1] }}") |
|||
assert tmpl.render(foo=[0, 1, 2]) == '0|2' |
|||
|
|||
def test_tuple(self): |
|||
tmpl = env.from_string('{{ () }}|{{ (1,) }}|{{ (1, 2) }}') |
|||
assert tmpl.render() == '()|(1,)|(1, 2)' |
|||
|
|||
def test_math(self): |
|||
tmpl = env.from_string('{{ (1 + 1 * 2) - 3 / 2 }}|{{ 2**3 }}') |
|||
assert tmpl.render() == '1.5|8' |
|||
|
|||
def test_div(self): |
|||
tmpl = env.from_string('{{ 3 // 2 }}|{{ 3 / 2 }}|{{ 3 % 2 }}') |
|||
assert tmpl.render() == '1|1.5|1' |
|||
|
|||
def test_unary(self): |
|||
tmpl = env.from_string('{{ +3 }}|{{ -3 }}') |
|||
assert tmpl.render() == '3|-3' |
|||
|
|||
def test_concat(self): |
|||
tmpl = env.from_string("{{ [1, 2] ~ 'foo' }}") |
|||
assert tmpl.render() == '[1, 2]foo' |
|||
|
|||
def test_compare(self): |
|||
tmpl = env.from_string('{{ 1 > 0 }}|{{ 1 >= 1 }}|{{ 2 < 3 }}|' |
|||
'{{ 2 == 2 }}|{{ 1 <= 1 }}') |
|||
assert tmpl.render() == 'True|True|True|True|True' |
|||
|
|||
def test_inop(self): |
|||
tmpl = env.from_string('{{ 1 in [1, 2, 3] }}|{{ 1 not in [1, 2, 3] }}') |
|||
assert tmpl.render() == 'True|False' |
|||
|
|||
def test_literals(self): |
|||
tmpl = env.from_string('{{ [] }}|{{ {} }}|{{ () }}') |
|||
assert tmpl.render().lower() == '[]|{}|()' |
|||
|
|||
def test_bool(self): |
|||
tmpl = env.from_string('{{ true and false }}|{{ false ' |
|||
'or true }}|{{ not false }}') |
|||
assert tmpl.render() == 'False|True|True' |
|||
|
|||
def test_grouping(self): |
|||
tmpl = env.from_string('{{ (true and false) or (false and true) and not false }}') |
|||
assert tmpl.render() == 'False' |
|||
|
|||
def test_django_attr(self): |
|||
tmpl = env.from_string('{{ [1, 2, 3].0 }}|{{ [[1]].0.0 }}') |
|||
assert tmpl.render() == '1|1' |
|||
|
|||
def test_conditional_expression(self): |
|||
tmpl = env.from_string('''{{ 0 if true else 1 }}''') |
|||
assert tmpl.render() == '0' |
|||
|
|||
def test_short_conditional_expression(self): |
|||
tmpl = env.from_string('<{{ 1 if false }}>') |
|||
assert tmpl.render() == '<>' |
|||
|
|||
tmpl = env.from_string('<{{ (1 if false).bar }}>') |
|||
self.assert_raises(UndefinedError, tmpl.render) |
|||
|
|||
def test_filter_priority(self): |
|||
tmpl = env.from_string('{{ "foo"|upper + "bar"|upper }}') |
|||
assert tmpl.render() == 'FOOBAR' |
|||
|
|||
def test_function_calls(self): |
|||
tests = [ |
|||
(True, '*foo, bar'), |
|||
(True, '*foo, *bar'), |
|||
(True, '*foo, bar=42'), |
|||
(True, '**foo, *bar'), |
|||
(True, '**foo, bar'), |
|||
(False, 'foo, bar'), |
|||
(False, 'foo, bar=42'), |
|||
(False, 'foo, bar=23, *args'), |
|||
(False, 'a, b=c, *d, **e'), |
|||
(False, '*foo, **bar') |
|||
] |
|||
for should_fail, sig in tests: |
|||
if should_fail: |
|||
self.assert_raises(TemplateSyntaxError, |
|||
env.from_string, '{{ foo(%s) }}' % sig) |
|||
else: |
|||
env.from_string('foo(%s)' % sig) |
|||
|
|||
def test_tuple_expr(self): |
|||
for tmpl in [ |
|||
'{{ () }}', |
|||
'{{ (1, 2) }}', |
|||
'{{ (1, 2,) }}', |
|||
'{{ 1, }}', |
|||
'{{ 1, 2 }}', |
|||
'{% for foo, bar in seq %}...{% endfor %}', |
|||
'{% for x in foo, bar %}...{% endfor %}', |
|||
'{% for x in foo, %}...{% endfor %}' |
|||
]: |
|||
assert env.from_string(tmpl) |
|||
|
|||
def test_trailing_comma(self): |
|||
tmpl = env.from_string('{{ (1, 2,) }}|{{ [1, 2,] }}|{{ {1: 2,} }}') |
|||
assert tmpl.render().lower() == '(1, 2)|[1, 2]|{1: 2}' |
|||
|
|||
def test_block_end_name(self): |
|||
env.from_string('{% block foo %}...{% endblock foo %}') |
|||
self.assert_raises(TemplateSyntaxError, env.from_string, |
|||
'{% block x %}{% endblock y %}') |
|||
|
|||
def test_contant_casing(self): |
|||
for const in True, False, None: |
|||
tmpl = env.from_string('{{ %s }}|{{ %s }}|{{ %s }}' % ( |
|||
str(const), str(const).lower(), str(const).upper() |
|||
)) |
|||
assert tmpl.render() == '%s|%s|' % (const, const) |
|||
|
|||
def test_test_chaining(self): |
|||
self.assert_raises(TemplateSyntaxError, env.from_string, |
|||
'{{ foo is string is sequence }}') |
|||
env.from_string('{{ 42 is string or 42 is number }}' |
|||
).render() == 'True' |
|||
|
|||
def test_string_concatenation(self): |
|||
tmpl = env.from_string('{{ "foo" "bar" "baz" }}') |
|||
assert tmpl.render() == 'foobarbaz' |
|||
|
|||
def test_notin(self): |
|||
bar = xrange(100) |
|||
tmpl = env.from_string('''{{ not 42 in bar }}''') |
|||
assert tmpl.render(bar=bar) == unicode(not 42 in bar) |
|||
|
|||
def test_implicit_subscribed_tuple(self): |
|||
class Foo(object): |
|||
def __getitem__(self, x): |
|||
return x |
|||
t = env.from_string('{{ foo[1, 2] }}') |
|||
assert t.render(foo=Foo()) == u'(1, 2)' |
|||
|
|||
def test_raw2(self): |
|||
tmpl = env.from_string('{% raw %}{{ FOO }} and {% BAR %}{% endraw %}') |
|||
assert tmpl.render() == '{{ FOO }} and {% BAR %}' |
|||
|
|||
def test_const(self): |
|||
tmpl = env.from_string('{{ true }}|{{ false }}|{{ none }}|' |
|||
'{{ none is defined }}|{{ missing is defined }}') |
|||
assert tmpl.render() == 'True|False|None|True|False' |
|||
|
|||
def test_neg_filter_priority(self): |
|||
node = env.parse('{{ -1|foo }}') |
|||
assert isinstance(node.body[0].nodes[0], nodes.Filter) |
|||
assert isinstance(node.body[0].nodes[0].node, nodes.Neg) |
|||
|
|||
def test_const_assign(self): |
|||
constass1 = '''{% set true = 42 %}''' |
|||
constass2 = '''{% for none in seq %}{% endfor %}''' |
|||
for tmpl in constass1, constass2: |
|||
self.assert_raises(TemplateSyntaxError, env.from_string, tmpl) |
|||
|
|||
def test_localset(self): |
|||
tmpl = env.from_string('''{% set foo = 0 %}\ |
|||
{% for item in [1, 2] %}{% set foo = 1 %}{% endfor %}\ |
|||
{{ foo }}''') |
|||
assert tmpl.render() == '0' |
|||
|
|||
def test_parse_unary(self): |
|||
tmpl = env.from_string('{{ -foo["bar"] }}') |
|||
assert tmpl.render(foo={'bar': 42}) == '-42' |
|||
tmpl = env.from_string('{{ -foo["bar"]|abs }}') |
|||
assert tmpl.render(foo={'bar': 42}) == '42' |
|||
|
|||
|
|||
def suite(): |
|||
suite = unittest.TestSuite() |
|||
suite.addTest(unittest.makeSuite(LexerTestCase)) |
|||
suite.addTest(unittest.makeSuite(ParserTestCase)) |
|||
suite.addTest(unittest.makeSuite(SyntaxTestCase)) |
|||
return suite |
@ -1,190 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
jinja2.testsuite.loader |
|||
~~~~~~~~~~~~~~~~~~~~~~~ |
|||
|
|||
Test the loaders. |
|||
|
|||
:copyright: (c) 2010 by the Jinja Team. |
|||
:license: BSD, see LICENSE for more details. |
|||
""" |
|||
import os |
|||
import sys |
|||
import tempfile |
|||
import shutil |
|||
import unittest |
|||
|
|||
from jinja2.testsuite import JinjaTestCase, dict_loader, \ |
|||
package_loader, filesystem_loader, function_loader, \ |
|||
choice_loader, prefix_loader |
|||
|
|||
from jinja2 import Environment, loaders |
|||
from jinja2.loaders import split_template_path |
|||
from jinja2.exceptions import TemplateNotFound |
|||
|
|||
|
|||
class LoaderTestCase(JinjaTestCase): |
|||
|
|||
def test_dict_loader(self): |
|||
env = Environment(loader=dict_loader) |
|||
tmpl = env.get_template('justdict.html') |
|||
assert tmpl.render().strip() == 'FOO' |
|||
self.assert_raises(TemplateNotFound, env.get_template, 'missing.html') |
|||
|
|||
def test_package_loader(self): |
|||
env = Environment(loader=package_loader) |
|||
tmpl = env.get_template('test.html') |
|||
assert tmpl.render().strip() == 'BAR' |
|||
self.assert_raises(TemplateNotFound, env.get_template, 'missing.html') |
|||
|
|||
def test_filesystem_loader(self): |
|||
env = Environment(loader=filesystem_loader) |
|||
tmpl = env.get_template('test.html') |
|||
assert tmpl.render().strip() == 'BAR' |
|||
tmpl = env.get_template('foo/test.html') |
|||
assert tmpl.render().strip() == 'FOO' |
|||
self.assert_raises(TemplateNotFound, env.get_template, 'missing.html') |
|||
|
|||
def test_choice_loader(self): |
|||
env = Environment(loader=choice_loader) |
|||
tmpl = env.get_template('justdict.html') |
|||
assert tmpl.render().strip() == 'FOO' |
|||
tmpl = env.get_template('test.html') |
|||
assert tmpl.render().strip() == 'BAR' |
|||
self.assert_raises(TemplateNotFound, env.get_template, 'missing.html') |
|||
|
|||
def test_function_loader(self): |
|||
env = Environment(loader=function_loader) |
|||
tmpl = env.get_template('justfunction.html') |
|||
assert tmpl.render().strip() == 'FOO' |
|||
self.assert_raises(TemplateNotFound, env.get_template, 'missing.html') |
|||
|
|||
def test_prefix_loader(self): |
|||
env = Environment(loader=prefix_loader) |
|||
tmpl = env.get_template('a/test.html') |
|||
assert tmpl.render().strip() == 'BAR' |
|||
tmpl = env.get_template('b/justdict.html') |
|||
assert tmpl.render().strip() == 'FOO' |
|||
self.assert_raises(TemplateNotFound, env.get_template, 'missing') |
|||
|
|||
def test_caching(self): |
|||
changed = False |
|||
class TestLoader(loaders.BaseLoader): |
|||
def get_source(self, environment, template): |
|||
return u'foo', None, lambda: not changed |
|||
env = Environment(loader=TestLoader(), cache_size=-1) |
|||
tmpl = env.get_template('template') |
|||
assert tmpl is env.get_template('template') |
|||
changed = True |
|||
assert tmpl is not env.get_template('template') |
|||
changed = False |
|||
|
|||
env = Environment(loader=TestLoader(), cache_size=0) |
|||
assert env.get_template('template') \ |
|||
is not env.get_template('template') |
|||
|
|||
env = Environment(loader=TestLoader(), cache_size=2) |
|||
t1 = env.get_template('one') |
|||
t2 = env.get_template('two') |
|||
assert t2 is env.get_template('two') |
|||
assert t1 is env.get_template('one') |
|||
t3 = env.get_template('three') |
|||
assert 'one' in env.cache |
|||
assert 'two' not in env.cache |
|||
assert 'three' in env.cache |
|||
|
|||
def test_split_template_path(self): |
|||
assert split_template_path('foo/bar') == ['foo', 'bar'] |
|||
assert split_template_path('./foo/bar') == ['foo', 'bar'] |
|||
self.assert_raises(TemplateNotFound, split_template_path, '../foo') |
|||
|
|||
|
|||
class ModuleLoaderTestCase(JinjaTestCase): |
|||
archive = None |
|||
|
|||
def compile_down(self, zip='deflated', py_compile=False): |
|||
super(ModuleLoaderTestCase, self).setup() |
|||
log = [] |
|||
self.reg_env = Environment(loader=prefix_loader) |
|||
if zip is not None: |
|||
self.archive = tempfile.mkstemp(suffix='.zip')[1] |
|||
else: |
|||
self.archive = tempfile.mkdtemp() |
|||
self.reg_env.compile_templates(self.archive, zip=zip, |
|||
log_function=log.append, |
|||
py_compile=py_compile) |
|||
self.mod_env = Environment(loader=loaders.ModuleLoader(self.archive)) |
|||
return ''.join(log) |
|||
|
|||
def teardown(self): |
|||
super(ModuleLoaderTestCase, self).teardown() |
|||
if hasattr(self, 'mod_env'): |
|||
if os.path.isfile(self.archive): |
|||
os.remove(self.archive) |
|||
else: |
|||
shutil.rmtree(self.archive) |
|||
self.archive = None |
|||
|
|||
def test_log(self): |
|||
log = self.compile_down() |
|||
assert 'Compiled "a/foo/test.html" as ' \ |
|||
'tmpl_a790caf9d669e39ea4d280d597ec891c4ef0404a' in log |
|||
assert 'Finished compiling templates' in log |
|||
assert 'Could not compile "a/syntaxerror.html": ' \ |
|||
'Encountered unknown tag \'endif\'' in log |
|||
|
|||
def _test_common(self): |
|||
tmpl1 = self.reg_env.get_template('a/test.html') |
|||
tmpl2 = self.mod_env.get_template('a/test.html') |
|||
assert tmpl1.render() == tmpl2.render() |
|||
|
|||
tmpl1 = self.reg_env.get_template('b/justdict.html') |
|||
tmpl2 = self.mod_env.get_template('b/justdict.html') |
|||
assert tmpl1.render() == tmpl2.render() |
|||
|
|||
def test_deflated_zip_compile(self): |
|||
self.compile_down(zip='deflated') |
|||
self._test_common() |
|||
|
|||
def test_stored_zip_compile(self): |
|||
self.compile_down(zip='stored') |
|||
self._test_common() |
|||
|
|||
def test_filesystem_compile(self): |
|||
self.compile_down(zip=None) |
|||
self._test_common() |
|||
|
|||
def test_weak_references(self): |
|||
self.compile_down() |
|||
tmpl = self.mod_env.get_template('a/test.html') |
|||
key = loaders.ModuleLoader.get_template_key('a/test.html') |
|||
name = self.mod_env.loader.module.__name__ |
|||
|
|||
assert hasattr(self.mod_env.loader.module, key) |
|||
assert name in sys.modules |
|||
|
|||
# unset all, ensure the module is gone from sys.modules |
|||
self.mod_env = tmpl = None |
|||
|
|||
try: |
|||
import gc |
|||
gc.collect() |
|||
except: |
|||
pass |
|||
|
|||
assert name not in sys.modules |
|||
|
|||
def test_byte_compilation(self): |
|||
log = self.compile_down(py_compile=True) |
|||
assert 'Byte-compiled "a/test.html"' in log |
|||
tmpl1 = self.mod_env.get_template('a/test.html') |
|||
mod = self.mod_env.loader.module. \ |
|||
tmpl_3c4ddf650c1a73df961a6d3d2ce2752f1b8fd490 |
|||
assert mod.__file__.endswith('.pyc') |
|||
|
|||
|
|||
def suite(): |
|||
suite = unittest.TestSuite() |
|||
suite.addTest(unittest.makeSuite(LoaderTestCase)) |
|||
suite.addTest(unittest.makeSuite(ModuleLoaderTestCase)) |
|||
return suite |
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue