# mako/runtime.py
# Copyright (C) 2006-2016 the Mako authors and contributors <see AUTHORS file>
#
# This module is part of Mako and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php

"""provides runtime services for templates, including Context,
Namespace, and various helper functions."""

from mako import exceptions, util, compat
from mako.compat import compat_builtins
import sys


class Context(object):

    """Provides runtime namespace, output buffer, and various
    callstacks for templates.

    See :ref:`runtime_toplevel` for detail on the usage of
    :class:`.Context`.

     """

    def __init__(self, buffer, **data):
        self._buffer_stack = [buffer]

        self._data = data

        self._kwargs = data.copy()
        self._with_template = None
        self._outputting_as_unicode = None
        self.namespaces = {}

        # "capture" function which proxies to the
        # generic "capture" function
        self._data['capture'] = compat.partial(capture, self)

        # "caller" stack used by def calls with content
        self.caller_stack = self._data['caller'] = CallerStack()

    def _set_with_template(self, t):
        self._with_template = t
        illegal_names = t.reserved_names.intersection(self._data)
        if illegal_names:
            raise exceptions.NameConflictError(
                "Reserved words passed to render(): %s" %
                ", ".join(illegal_names))

    @property
    def lookup(self):
        """Return the :class:`.TemplateLookup` associated
        with this :class:`.Context`.

        """
        return self._with_template.lookup

    @property
    def kwargs(self):
        """Return the dictionary of top level keyword arguments associated
        with this :class:`.Context`.

        This dictionary only includes the top-level arguments passed to
        :meth:`.Template.render`.  It does not include names produced within
        the template execution such as local variable names or special names
        such as ``self``, ``next``, etc.

        The purpose of this dictionary is primarily for the case that
        a :class:`.Template` accepts arguments via its ``<%page>`` tag,
        which are normally expected to be passed via :meth:`.Template.render`,
        except the template is being called in an inheritance context,
        using the ``body()`` method.   :attr:`.Context.kwargs` can then be
        used to propagate these arguments to the inheriting template::

            ${next.body(**context.kwargs)}

        """
        return self._kwargs.copy()

    def push_caller(self, caller):
        """Push a ``caller`` callable onto the callstack for
        this :class:`.Context`."""

        self.caller_stack.append(caller)

    def pop_caller(self):
        """Pop a ``caller`` callable onto the callstack for this
        :class:`.Context`."""

        del self.caller_stack[-1]

    def keys(self):
        """Return a list of all names established in this :class:`.Context`."""

        return list(self._data.keys())

    def __getitem__(self, key):
        if key in self._data:
            return self._data[key]
        else:
            return compat_builtins.__dict__[key]

    def _push_writer(self):
        """push a capturing buffer onto this Context and return
        the new writer function."""

        buf = util.FastEncodingBuffer()
        self._buffer_stack.append(buf)
        return buf.write

    def _pop_buffer_and_writer(self):
        """pop the most recent capturing buffer from this Context
        and return the current writer after the pop.

        """

        buf = self._buffer_stack.pop()
        return buf, self._buffer_stack[-1].write

    def _push_buffer(self):
        """push a capturing buffer onto this Context."""

        self._push_writer()

    def _pop_buffer(self):
        """pop the most recent capturing buffer from this Context."""

        return self._buffer_stack.pop()

    def get(self, key, default=None):
        """Return a value from this :class:`.Context`."""

        return self._data.get(key, compat_builtins.__dict__.get(key, default))

    def write(self, string):
        """Write a string to this :class:`.Context` object's
        underlying output buffer."""

        self._buffer_stack[-1].write(string)

    def writer(self):
        """Return the current writer function."""

        return self._buffer_stack[-1].write

    def _copy(self):
        c = Context.__new__(Context)
        c._buffer_stack = self._buffer_stack
        c._data = self._data.copy()
        c._kwargs = self._kwargs
        c._with_template = self._with_template
        c._outputting_as_unicode = self._outputting_as_unicode
        c.namespaces = self.namespaces
        c.caller_stack = self.caller_stack
        return c

    def _locals(self, d):
        """Create a new :class:`.Context` with a copy of this
        :class:`.Context`'s current state,
        updated with the given dictionary.

        The :attr:`.Context.kwargs` collection remains
        unaffected.


        """

        if not d:
            return self
        c = self._copy()
        c._data.update(d)
        return c

    def _clean_inheritance_tokens(self):
        """create a new copy of this :class:`.Context`. with
        tokens related to inheritance state removed."""

        c = self._copy()
        x = c._data
        x.pop('self', None)
        x.pop('parent', None)
        x.pop('next', None)
        return c


class CallerStack(list):

    def __init__(self):
        self.nextcaller = None

    def __nonzero__(self):
        return self.__bool__()

    def __bool__(self):
        return len(self) and self._get_caller() and True or False

    def _get_caller(self):
        # this method can be removed once
        # codegen MAGIC_NUMBER moves past 7
        return self[-1]

    def __getattr__(self, key):
        return getattr(self._get_caller(), key)

    def _push_frame(self):
        frame = self.nextcaller or None
        self.append(frame)
        self.nextcaller = None
        return frame

    def _pop_frame(self):
        self.nextcaller = self.pop()


class Undefined(object):

    """Represents an undefined value in a template.

    All template modules have a constant value
    ``UNDEFINED`` present which is an instance of this
    object.

    """

    def __str__(self):
        raise NameError("Undefined")

    def __nonzero__(self):
        return self.__bool__()

    def __bool__(self):
        return False

UNDEFINED = Undefined()
STOP_RENDERING = ""


class LoopStack(object):

    """a stack for LoopContexts that implements the context manager protocol
    to automatically pop off the top of the stack on context exit
    """

    def __init__(self):
        self.stack = []

    def _enter(self, iterable):
        self._push(iterable)
        return self._top

    def _exit(self):
        self._pop()
        return self._top

    @property
    def _top(self):
        if self.stack:
            return self.stack[-1]
        else:
            return self

    def _pop(self):
        return self.stack.pop()

    def _push(self, iterable):
        new = LoopContext(iterable)
        if self.stack:
            new.parent = self.stack[-1]
        return self.stack.append(new)

    def __getattr__(self, key):
        raise exceptions.RuntimeException("No loop context is established")

    def __iter__(self):
        return iter(self._top)


class LoopContext(object):

    """A magic loop variable.
    Automatically accessible in any ``% for`` block.

    See the section :ref:`loop_context` for usage
    notes.

    :attr:`parent` -> :class:`.LoopContext` or ``None``
        The parent loop, if one exists.
    :attr:`index` -> `int`
        The 0-based iteration count.
    :attr:`reverse_index` -> `int`
        The number of iterations remaining.
    :attr:`first` -> `bool`
        ``True`` on the first iteration, ``False`` otherwise.
    :attr:`last` -> `bool`
        ``True`` on the last iteration, ``False`` otherwise.
    :attr:`even` -> `bool`
        ``True`` when ``index`` is even.
    :attr:`odd` -> `bool`
        ``True`` when ``index`` is odd.
    """

    def __init__(self, iterable):
        self._iterable = iterable
        self.index = 0
        self.parent = None

    def __iter__(self):
        for i in self._iterable:
            yield i
            self.index += 1

    @util.memoized_instancemethod
    def __len__(self):
        return len(self._iterable)

    @property
    def reverse_index(self):
        return len(self) - self.index - 1

    @property
    def first(self):
        return self.index == 0

    @property
    def last(self):
        return self.index == len(self) - 1

    @property
    def even(self):
        return not self.odd

    @property
    def odd(self):
        return bool(self.index % 2)

    def cycle(self, *values):
        """Cycle through values as the loop progresses.
        """
        if not values:
            raise ValueError("You must provide values to cycle through")
        return values[self.index % len(values)]


class _NSAttr(object):

    def __init__(self, parent):
        self.__parent = parent

    def __getattr__(self, key):
        ns = self.__parent
        while ns:
            if hasattr(ns.module, key):
                return getattr(ns.module, key)
            else:
                ns = ns.inherits
        raise AttributeError(key)


class Namespace(object):

    """Provides access to collections of rendering methods, which
      can be local, from other templates, or from imported modules.

      To access a particular rendering method referenced by a
      :class:`.Namespace`, use plain attribute access:

      .. sourcecode:: mako

        ${some_namespace.foo(x, y, z)}

      :class:`.Namespace` also contains several built-in attributes
      described here.

      """

    def __init__(self, name, context,
                 callables=None, inherits=None,
                 populate_self=True, calling_uri=None):
        self.name = name
        self.context = context
        self.inherits = inherits
        if callables is not None:
            self.callables = dict([(c.__name__, c) for c in callables])

    callables = ()

    module = None
    """The Python module referenced by this :class:`.Namespace`.

    If the namespace references a :class:`.Template`, then
    this module is the equivalent of ``template.module``,
    i.e. the generated module for the template.

    """

    template = None
    """The :class:`.Template` object referenced by this
        :class:`.Namespace`, if any.

    """

    context = None
    """The :class:`.Context` object for this :class:`.Namespace`.

    Namespaces are often created with copies of contexts that
    contain slightly different data, particularly in inheritance
    scenarios. Using the :class:`.Context` off of a :class:`.Namespace` one
    can traverse an entire chain of templates that inherit from
    one-another.

    """

    filename = None
    """The path of the filesystem file used for this
    :class:`.Namespace`'s module or template.

    If this is a pure module-based
    :class:`.Namespace`, this evaluates to ``module.__file__``. If a
    template-based namespace, it evaluates to the original
    template file location.

    """

    uri = None
    """The URI for this :class:`.Namespace`'s template.

    I.e. whatever was sent to :meth:`.TemplateLookup.get_template()`.

    This is the equivalent of :attr:`.Template.uri`.

    """

    _templateuri = None

    @util.memoized_property
    def attr(self):
        """Access module level attributes by name.

        This accessor allows templates to supply "scalar"
        attributes which are particularly handy in inheritance
        relationships.

        .. seealso::

            :ref:`inheritance_attr`

            :ref:`namespace_attr_for_includes`

        """
        return _NSAttr(self)

    def get_namespace(self, uri):
        """Return a :class:`.Namespace` corresponding to the given ``uri``.

        If the given ``uri`` is a relative URI (i.e. it does not
        contain a leading slash ``/``), the ``uri`` is adjusted to
        be relative to the ``uri`` of the namespace itself. This
        method is therefore mostly useful off of the built-in
        ``local`` namespace, described in :ref:`namespace_local`.

        In
        most cases, a template wouldn't need this function, and
        should instead use the ``<%namespace>`` tag to load
        namespaces. However, since all ``<%namespace>`` tags are
        evaluated before the body of a template ever runs,
        this method can be used to locate namespaces using
        expressions that were generated within the body code of
        the template, or to conditionally use a particular
        namespace.

        """
        key = (self, uri)
        if key in self.context.namespaces:
            return self.context.namespaces[key]
        else:
            ns = TemplateNamespace(uri, self.context._copy(),
                                   templateuri=uri,
                                   calling_uri=self._templateuri)
            self.context.namespaces[key] = ns
            return ns

    def get_template(self, uri):
        """Return a :class:`.Template` from the given ``uri``.

        The ``uri`` resolution is relative to the ``uri`` of this
        :class:`.Namespace` object's :class:`.Template`.

        """
        return _lookup_template(self.context, uri, self._templateuri)

    def get_cached(self, key, **kwargs):
        """Return a value from the :class:`.Cache` referenced by this
        :class:`.Namespace` object's :class:`.Template`.

        The advantage to this method versus direct access to the
        :class:`.Cache` is that the configuration parameters
        declared in ``<%page>`` take effect here, thereby calling
        up the same configured backend as that configured
        by ``<%page>``.

        """

        return self.cache.get(key, **kwargs)

    @property
    def cache(self):
        """Return the :class:`.Cache` object referenced
        by this :class:`.Namespace` object's
        :class:`.Template`.

        """
        return self.template.cache

    def include_file(self, uri, **kwargs):
        """Include a file at the given ``uri``."""

        _include_file(self.context, uri, self._templateuri, **kwargs)

    def _populate(self, d, l):
        for ident in l:
            if ident == '*':
                for (k, v) in self._get_star():
                    d[k] = v
            else:
                d[ident] = getattr(self, ident)

    def _get_star(self):
        if self.callables:
            for key in self.callables:
                yield (key, self.callables[key])

    def __getattr__(self, key):
        if key in self.callables:
            val = self.callables[key]
        elif self.inherits:
            val = getattr(self.inherits, key)
        else:
            raise AttributeError(
                "Namespace '%s' has no member '%s'" %
                (self.name, key))
        setattr(self, key, val)
        return val


class TemplateNamespace(Namespace):

    """A :class:`.Namespace` specific to a :class:`.Template` instance."""

    def __init__(self, name, context, template=None, templateuri=None,
                 callables=None, inherits=None,
                 populate_self=True, calling_uri=None):
        self.name = name
        self.context = context
        self.inherits = inherits
        if callables is not None:
            self.callables = dict([(c.__name__, c) for c in callables])

        if templateuri is not None:
            self.template = _lookup_template(context, templateuri,
                                             calling_uri)
            self._templateuri = self.template.module._template_uri
        elif template is not None:
            self.template = template
            self._templateuri = template.module._template_uri
        else:
            raise TypeError("'template' argument is required.")

        if populate_self:
            lclcallable, lclcontext = \
                _populate_self_namespace(context, self.template,
                                         self_ns=self)

    @property
    def module(self):
        """The Python module referenced by this :class:`.Namespace`.

        If the namespace references a :class:`.Template`, then
        this module is the equivalent of ``template.module``,
        i.e. the generated module for the template.

        """
        return self.template.module

    @property
    def filename(self):
        """The path of the filesystem file used for this
        :class:`.Namespace`'s module or template.
        """
        return self.template.filename

    @property
    def uri(self):
        """The URI for this :class:`.Namespace`'s template.

        I.e. whatever was sent to :meth:`.TemplateLookup.get_template()`.

        This is the equivalent of :attr:`.Template.uri`.

        """
        return self.template.uri

    def _get_star(self):
        if self.callables:
            for key in self.callables:
                yield (key, self.callables[key])

        def get(key):
            callable_ = self.template._get_def_callable(key)
            return compat.partial(callable_, self.context)
        for k in self.template.module._exports:
            yield (k, get(k))

    def __getattr__(self, key):
        if key in self.callables:
            val = self.callables[key]
        elif self.template.has_def(key):
            callable_ = self.template._get_def_callable(key)
            val = compat.partial(callable_, self.context)
        elif self.inherits:
            val = getattr(self.inherits, key)

        else:
            raise AttributeError(
                "Namespace '%s' has no member '%s'" %
                (self.name, key))
        setattr(self, key, val)
        return val


class ModuleNamespace(Namespace):

    """A :class:`.Namespace` specific to a Python module instance."""

    def __init__(self, name, context, module,
                 callables=None, inherits=None,
                 populate_self=True, calling_uri=None):
        self.name = name
        self.context = context
        self.inherits = inherits
        if callables is not None:
            self.callables = dict([(c.__name__, c) for c in callables])

        mod = __import__(module)
        for token in module.split('.')[1:]:
            mod = getattr(mod, token)
        self.module = mod

    @property
    def filename(self):
        """The path of the filesystem file used for this
        :class:`.Namespace`'s module or template.
        """
        return self.module.__file__

    def _get_star(self):
        if self.callables:
            for key in self.callables:
                yield (key, self.callables[key])
        for key in dir(self.module):
            if key[0] != '_':
                callable_ = getattr(self.module, key)
                if compat.callable(callable_):
                    yield key, compat.partial(callable_, self.context)

    def __getattr__(self, key):
        if key in self.callables:
            val = self.callables[key]
        elif hasattr(self.module, key):
            callable_ = getattr(self.module, key)
            val = compat.partial(callable_, self.context)
        elif self.inherits:
            val = getattr(self.inherits, key)
        else:
            raise AttributeError(
                "Namespace '%s' has no member '%s'" %
                (self.name, key))
        setattr(self, key, val)
        return val


def supports_caller(func):
    """Apply a caller_stack compatibility decorator to a plain
    Python function.

    See the example in :ref:`namespaces_python_modules`.

    """

    def wrap_stackframe(context, *args, **kwargs):
        context.caller_stack._push_frame()
        try:
            return func(context, *args, **kwargs)
        finally:
            context.caller_stack._pop_frame()
    return wrap_stackframe


def capture(context, callable_, *args, **kwargs):
    """Execute the given template def, capturing the output into
    a buffer.

    See the example in :ref:`namespaces_python_modules`.

    """

    if not compat.callable(callable_):
        raise exceptions.RuntimeException(
            "capture() function expects a callable as "
            "its argument (i.e. capture(func, *args, **kwargs))"
        )
    context._push_buffer()
    try:
        callable_(*args, **kwargs)
    finally:
        buf = context._pop_buffer()
    return buf.getvalue()


def _decorate_toplevel(fn):
    def decorate_render(render_fn):
        def go(context, *args, **kw):
            def y(*args, **kw):
                return render_fn(context, *args, **kw)
            try:
                y.__name__ = render_fn.__name__[7:]
            except TypeError:
                # < Python 2.4
                pass
            return fn(y)(context, *args, **kw)
        return go
    return decorate_render


def _decorate_inline(context, fn):
    def decorate_render(render_fn):
        dec = fn(render_fn)

        def go(*args, **kw):
            return dec(context, *args, **kw)
        return go
    return decorate_render


def _include_file(context, uri, calling_uri, **kwargs):
    """locate the template from the given uri and include it in
    the current output."""

    template = _lookup_template(context, uri, calling_uri)
    (callable_, ctx) = _populate_self_namespace(
        context._clean_inheritance_tokens(),
        template)
    callable_(ctx, **_kwargs_for_include(callable_, context._data, **kwargs))


def _inherit_from(context, uri, calling_uri):
    """called by the _inherit method in template modules to set
    up the inheritance chain at the start of a template's
    execution."""

    if uri is None:
        return None
    template = _lookup_template(context, uri, calling_uri)
    self_ns = context['self']
    ih = self_ns
    while ih.inherits is not None:
        ih = ih.inherits
    lclcontext = context._locals({'next': ih})
    ih.inherits = TemplateNamespace("self:%s" % template.uri,
                                    lclcontext,
                                    template=template,
                                    populate_self=False)
    context._data['parent'] = lclcontext._data['local'] = ih.inherits
    callable_ = getattr(template.module, '_mako_inherit', None)
    if callable_ is not None:
        ret = callable_(template, lclcontext)
        if ret:
            return ret

    gen_ns = getattr(template.module, '_mako_generate_namespaces', None)
    if gen_ns is not None:
        gen_ns(context)
    return (template.callable_, lclcontext)


def _lookup_template(context, uri, relativeto):
    lookup = context._with_template.lookup
    if lookup is None:
        raise exceptions.TemplateLookupException(
            "Template '%s' has no TemplateLookup associated" %
            context._with_template.uri)
    uri = lookup.adjust_uri(uri, relativeto)
    try:
        return lookup.get_template(uri)
    except exceptions.TopLevelLookupException:
        raise exceptions.TemplateLookupException(str(compat.exception_as()))


def _populate_self_namespace(context, template, self_ns=None):
    if self_ns is None:
        self_ns = TemplateNamespace('self:%s' % template.uri,
                                    context, template=template,
                                    populate_self=False)
    context._data['self'] = context._data['local'] = self_ns
    if hasattr(template.module, '_mako_inherit'):
        ret = template.module._mako_inherit(template, context)
        if ret:
            return ret
    return (template.callable_, context)


def _render(template, callable_, args, data, as_unicode=False):
    """create a Context and return the string
    output of the given template and template callable."""

    if as_unicode:
        buf = util.FastEncodingBuffer(as_unicode=True)
    elif template.bytestring_passthrough:
        buf = compat.StringIO()
    else:
        buf = util.FastEncodingBuffer(
            as_unicode=as_unicode,
            encoding=template.output_encoding,
            errors=template.encoding_errors)
    context = Context(buf, **data)
    context._outputting_as_unicode = as_unicode
    context._set_with_template(template)

    _render_context(template, callable_, context, *args,
                    **_kwargs_for_callable(callable_, data))
    return context._pop_buffer().getvalue()


def _kwargs_for_callable(callable_, data):
    argspec = compat.inspect_func_args(callable_)
    # for normal pages, **pageargs is usually present
    if argspec[2]:
        return data

    # for rendering defs from the top level, figure out the args
    namedargs = argspec[0] + [v for v in argspec[1:3] if v is not None]
    kwargs = {}
    for arg in namedargs:
        if arg != 'context' and arg in data and arg not in kwargs:
            kwargs[arg] = data[arg]
    return kwargs


def _kwargs_for_include(callable_, data, **kwargs):
    argspec = compat.inspect_func_args(callable_)
    namedargs = argspec[0] + [v for v in argspec[1:3] if v is not None]
    for arg in namedargs:
        if arg != 'context' and arg in data and arg not in kwargs:
            kwargs[arg] = data[arg]
    return kwargs


def _render_context(tmpl, callable_, context, *args, **kwargs):
    import mako.template as template
    # create polymorphic 'self' namespace for this
    # template with possibly updated context
    if not isinstance(tmpl, template.DefTemplate):
        # if main render method, call from the base of the inheritance stack
        (inherit, lclcontext) = _populate_self_namespace(context, tmpl)
        _exec_template(inherit, lclcontext, args=args, kwargs=kwargs)
    else:
        # otherwise, call the actual rendering method specified
        (inherit, lclcontext) = _populate_self_namespace(context, tmpl.parent)
        _exec_template(callable_, context, args=args, kwargs=kwargs)


def _exec_template(callable_, context, args=None, kwargs=None):
    """execute a rendering callable given the callable, a
    Context, and optional explicit arguments

    the contextual Template will be located if it exists, and
    the error handling options specified on that Template will
    be interpreted here.
    """
    template = context._with_template
    if template is not None and \
            (template.format_exceptions or template.error_handler):
        try:
            callable_(context, *args, **kwargs)
        except Exception:
            _render_error(template, context, compat.exception_as())
        except:
            e = sys.exc_info()[0]
            _render_error(template, context, e)
    else:
        callable_(context, *args, **kwargs)


def _render_error(template, context, error):
    if template.error_handler:
        result = template.error_handler(context, error)
        if not result:
            compat.reraise(*sys.exc_info())
    else:
        error_template = exceptions.html_error_template()
        if context._outputting_as_unicode:
            context._buffer_stack[:] = [
                util.FastEncodingBuffer(as_unicode=True)]
        else:
            context._buffer_stack[:] = [util.FastEncodingBuffer(
                error_template.output_encoding,
                error_template.encoding_errors)]

        context._set_with_template(error_template)
        error_template.render_context(context, error=error)
