Edgewall Software

Changeset 819


Ignore:
Timestamp:
Apr 1, 2008, 12:47:50 AM (16 years ago)
Author:
aflett
Message:

merge in trunk up through r818 - fundamentally changed the way MatchSet? works, but actually is more consistent now

Location:
branches/experimental/match-fastpaths
Files:
1 added
22 edited

Legend:

Unmodified
Added
Removed
  • branches/experimental/match-fastpaths/ChangeLog

    r798 r819  
    4747 * Assigning to a variable named `data` in a Python code block no longer
    4848   breaks context lookup.
     49 * The `Stream.render` now accepts an optional `out` parameter that can be
     50   used to pass in a writable file-like object to use for assembling the
     51   output, instead of building a big string and returning it.
     52 * The XHTML serializer now strips `xml:space` attributes as they are only
     53   allowed on very few tags.
     54 * Match templates are now applied in a more controlled fashion: in the order
     55   they are declared in the template source, all match templates up to (and
     56   including) the matching template itself are applied to the matched content,
     57   whereas the match templates declared after the matching template are only
     58   applied to the generated content (ticket #186).
     59 * The `TemplateLoader` class now provides an `instantiate()` method that can
     60   be overridden by subclasses to implement advanced template instantiation
     61   logic (ticket #204).
     62 * The search path of the `TemplateLoader` class can now contain ''load
     63   functions'' in addition to path strings. A load function is passed the
     64   name of the requested template file, and should return a file-like object
     65   and some metadata. New load functions are supplied for loading from egg
     66   package data, and loading from different loaders depending on the path
     67   prefix of the requested filename (ticket #182).
     68 * Match templates can now be processed without keeping the complete matched
     69   content in memory, which could cause excessive memory use on long pages.
     70   The buffering can be disabled using the new `buffer` optimization hint on
     71   the `<py:match>` directive.
     72 * Improve error reporting when accessing an attribute in a Python expression
     73   raises an `AttributeError` (ticket #191).
    4974
    5075
  • branches/experimental/match-fastpaths/doc/templates.txt

    r765 r819  
    271271using plain expressions.
    272272
     273.. warning:: Unfortunately, code blocks are severely limited when running
     274             under Python 2.3: For example, it is not possible to access
     275             variables defined in outer scopes. If you plan to use code blocks
     276             extensively, it is strongly recommended that you run Python 2.4
     277             or later.
     278
    273279
    274280.. _`error handling`:
  • branches/experimental/match-fastpaths/doc/text-templates.txt

    r706 r819  
    216216.. code-block:: genshitext
    217217
    218   {% include '%s.txt' % filename %}
     218  {% include ${'%s.txt' % filename} %}
    219219
    220220Note that a ``TemplateNotFound`` exception is raised if an included file can't
     
    350350end of a directive is removed automatically.
    351351
    352 .. note:: If you're using this old syntax, it is strongly recommended to migrate
    353           to the new syntax. Simply replace any references to ``TextTemplate``
    354           by ``NewTextTemplate``. On the other hand, if you want to stick with
    355           the old syntax for a while longer, replace references to
     352.. note:: If you're using this old syntax, it is strongly recommended to
     353          migrate to the new syntax. Simply replace any references to
     354          ``TextTemplate`` by ``NewTextTemplate`` (and also change the
     355          text templates, of course). On the other hand, if you want to stick
     356          with the old syntax for a while longer, replace references to
    356357          ``TextTemplate`` by ``OldTextTemplate``; while ``TextTemplate`` is
    357358          still an alias for the old language at this point, that will change
    358           in a future release.
     359          in a future release. But also note that the old syntax may be
     360          dropped entirely in a future release.
  • branches/experimental/match-fastpaths/doc/upgrade.txt

    r723 r819  
    3030warned that lenient error handling may be removed completely in a
    3131future release.
     32
     33There has also been a subtle change to how ``py:match`` templates are
     34processed: in previous versions, all match templates would be applied
     35to the content generated by the matching template, and only the
     36matching template itself was applied recursively to the original
     37content. This behavior resulted in problems with many kinds of
     38recursive matching, and hence was changed for 0.5: now, all match
     39templates declared before the matching template are applied to the
     40original content, and match templates declared after the matching
     41template are applied to the generated content. This change should not
     42have any effect on most applications, but you may want to check your
     43use of match templates to make sure.
    3244
    3345
  • branches/experimental/match-fastpaths/doc/xml-templates.txt

    r773 r819  
    323323.. _`Using XPath`: streams.html#using-xpath
    324324
     325Match templates are applied both to the original markup as well to the
     326generated markup. The order in which they are applied depends on the order
     327they are declared in the template source: a match template defined after
     328another match template is applied to the output generated by the first match
     329template. The match templates basically form a pipeline.
     330
    325331This directive can also be used as an element:
    326332
     
    353359| Attribute     | Default   | Description                                   |
    354360+===============+===========+===============================================+
     361| ``buffer``    | ``true``  | Whether the matched content should be         |
     362|               |           | buffered in memory. Buffering can improve     |
     363|               |           | performance a bit at the cost of needing more |
     364|               |           | memory during rendering. Buffering is         |
     365|               |           | ''required'' for match templates that contain |
     366|               |           | more than one invocation of the ``select()``  |
     367|               |           | function. If there is only one call, and the  |
     368|               |           | matched content can potentially be very long, |
     369|               |           | consider disabling buffering to avoid         |
     370|               |           | excessive memory use.                         |
     371+---------------+-----------+-----------------------------------------------+
    355372| ``once``      | ``false`` | Whether the engine should stop looking for    |
    356373|               |           | more matching elements after the first match. |
  • branches/experimental/match-fastpaths/genshi/core.py

    r801 r819  
    150150        return reduce(operator.or_, (self,) + filters)
    151151
    152     def render(self, method=None, encoding='utf-8', **kwargs):
     152    def render(self, method=None, encoding='utf-8', out=None, **kwargs):
    153153        """Return a string representation of the stream.
    154154       
     
    162162        :param encoding: how the output string should be encoded; if set to
    163163                         `None`, this method returns a `unicode` object
    164         :return: a `str` or `unicode` object
     164        :param out: a file-like object that the output should be written to
     165                    instead of being returned as one big string; note that if
     166                    this is a file or socket (or similar), the `encoding` must
     167                    not be `None` (that is, the output must be encoded)
     168        :return: a `str` or `unicode` object (depending on the `encoding`
     169                 parameter), or `None` if the `out` parameter is provided
    165170        :rtype: `basestring`
     171       
    166172        :see: XMLSerializer, XHTMLSerializer, HTMLSerializer, TextSerializer
     173        :note: Changed in 0.5: added the `out` parameter
    167174        """
    168175        from genshi.output import encode
     
    170177            method = self.serializer or 'xml'
    171178        generator = self.serialize(method=method, **kwargs)
    172         return encode(generator, method=method, encoding=encoding)
     179        return encode(generator, method=method, encoding=encoding, out=out)
    173180
    174181    def select(self, path, namespaces=None, variables=None):
  • branches/experimental/match-fastpaths/genshi/output.py

    r788 r819  
    3131__docformat__ = 'restructuredtext en'
    3232
    33 def encode(iterator, method='xml', encoding='utf-8'):
     33def encode(iterator, method='xml', encoding='utf-8', out=None):
    3434    """Encode serializer output into a string.
    3535   
     
    4040    :param encoding: how the output string should be encoded; if set to `None`,
    4141                     this method returns a `unicode` object
    42     :return: a string or unicode object (depending on the `encoding` parameter)
     42    :param out: a file-like object that the output should be written to
     43                instead of being returned as one big string; note that if
     44                this is a file or socket (or similar), the `encoding` must
     45                not be `None` (that is, the output must be encoded)
     46    :return: a `str` or `unicode` object (depending on the `encoding`
     47             parameter), or `None` if the `out` parameter is provided
     48   
    4349    :since: version 0.4.1
    44     """
    45     output = u''.join(list(iterator))
     50    :note: Changed in 0.5: added the `out` parameter
     51    """
    4652    if encoding is not None:
    4753        errors = 'replace'
    4854        if method != 'text' and not isinstance(method, TextSerializer):
    4955            errors = 'xmlcharrefreplace'
    50         return output.encode(encoding, errors)
    51     return output
     56        _encode = lambda string: string.encode(encoding, errors)
     57    else:
     58        _encode = lambda string: string
     59    if out is None:
     60        return _encode(u''.join(list(iterator)))
     61    for chunk in iterator:
     62        out.write(_encode(chunk))
    5263
    5364def get_serializer(method='xml', **kwargs):
     
    299310                    elif attr == u'xml:lang' and u'lang' not in attrib:
    300311                        buf += [' lang="', escape(value), '"']
     312                    elif attr == u'xml:space':
     313                        continue
    301314                    buf += [' ', attr, '="', escape(value), '"']
    302315                if kind is EMPTY:
  • branches/experimental/match-fastpaths/genshi/template/base.py

    r803 r819  
    256256
    257257
    258 def _apply_directives(stream, ctxt, directives):
     258def _apply_directives(stream, directives, ctxt, **vars):
    259259    """Apply the given directives to the stream.
    260260   
    261261    :param stream: the stream the directives should be applied to
     262    :param directives: the list of directives to apply
    262263    :param ctxt: the `Context`
    263     :param directives: the list of directives to apply
     264    :param vars: additional variables that should be available when Python
     265                 code is executed
    264266    :return: the stream with the given directives applied
    265267    """
    266268    if directives:
    267         stream = directives[0](iter(stream), ctxt, directives[1:])
     269        stream = directives[0](iter(stream), directives[1:], ctxt, **vars)
    268270    return stream
     271
     272def _eval_expr(expr, ctxt, **vars):
     273    """Evaluate the given `Expression` object.
     274   
     275    :param expr: the expression to evaluate
     276    :param ctxt: the `Context`
     277    :param vars: additional variables that should be available to the
     278                 expression
     279    :return: the result of the evaluation
     280    """
     281    if vars:
     282        ctxt.push(vars)
     283    retval = expr.evaluate(ctxt)
     284    if vars:
     285        ctxt.pop()
     286    return retval
     287
     288def _exec_suite(suite, ctxt, **vars):
     289    """Execute the given `Suite` object.
     290   
     291    :param suite: the code suite to execute
     292    :param ctxt: the `Context`
     293    :param vars: additional variables that should be available to the
     294                 code
     295    """
     296    if vars:
     297        ctxt.push(vars)
     298        ctxt.push({})
     299    suite.execute(_ctxt2dict(ctxt))
     300    if vars:
     301        top = ctxt.pop()
     302        ctxt.pop()
     303        ctxt.frames[0].update(top)
    269304
    270305
     
    428463                 the template to the context data.
    429464        """
     465        vars = {}
    430466        if args:
    431467            assert len(args) == 1
     
    433469            if ctxt is None:
    434470                ctxt = Context(**kwargs)
     471            else:
     472                vars = kwargs
    435473            assert isinstance(ctxt, Context)
    436474        else:
     
    439477        stream = self.stream
    440478        for filter_ in self.filters:
    441             stream = filter_(iter(stream), ctxt)
     479            stream = filter_(iter(stream), ctxt, **vars)
    442480        return Stream(stream, self.serializer)
    443481
    444     def _eval(self, stream, ctxt):
     482    def _eval(self, stream, ctxt, **vars):
    445483        """Internal stream filter that evaluates any expressions in `START` and
    446484        `TEXT` events.
     
    462500                        values = []
    463501                        for subkind, subdata, subpos in self._eval(substream,
    464                                                                    ctxt):
     502                                                                   ctxt,
     503                                                                   **vars):
    465504                            if subkind is TEXT:
    466505                                values.append(subdata)
     
    472511
    473512            elif kind is EXPR:
    474                 result = data.evaluate(ctxt)
     513                result = _eval_expr(data, ctxt, **vars)
    475514                if result is not None:
    476515                    # First check for a string, otherwise the iterable test
     
    484523                        substream = _ensure(result)
    485524                        for filter_ in filters:
    486                             substream = filter_(substream, ctxt)
     525                            substream = filter_(substream, ctxt, **vars)
    487526                        for event in substream:
    488527                            yield event
     
    493532                yield kind, data, pos
    494533
    495     def _exec(self, stream, ctxt):
     534    def _exec(self, stream, ctxt, **vars):
    496535        """Internal stream filter that executes Python code blocks."""
    497536        for event in stream:
    498537            if event[0] is EXEC:
    499                 event[1].execute(_ctxt2dict(ctxt))
     538                _exec_suite(event[1], ctxt, **vars)
    500539            else:
    501540                yield event
    502541
    503     def _flatten(self, stream, ctxt):
     542    def _flatten(self, stream, ctxt, **vars):
    504543        """Internal stream filter that expands `SUB` events in the stream."""
    505544        for event in stream:
     
    508547                # events to which those directives should be applied
    509548                directives, substream = event[1]
    510                 substream = _apply_directives(substream, ctxt, directives)
    511                 for event in self._flatten(substream, ctxt):
     549                substream = _apply_directives(substream, directives, ctxt,
     550                                              **vars)
     551                for event in self._flatten(substream, ctxt, **vars):
    512552                    yield event
    513553            else:
    514554                yield event
    515555
    516     def _include(self, stream, ctxt):
     556    def _include(self, stream, ctxt, **vars):
    517557        """Internal stream filter that performs inclusion of external
    518558        template files.
     
    525565                if not isinstance(href, basestring):
    526566                    parts = []
    527                     for subkind, subdata, subpos in self._eval(href, ctxt):
     567                    for subkind, subdata, subpos in self._eval(href, ctxt,
     568                                                               **vars):
    528569                        if subkind is TEXT:
    529570                            parts.append(subdata)
     
    532573                    tmpl = self.loader.load(href, relative_to=event[2][0],
    533574                                            cls=cls or self.__class__)
    534                     for event in tmpl.generate(ctxt):
     575                    for event in tmpl.generate(ctxt, **vars):
    535576                        yield event
    536577                except TemplateNotFound:
     
    538579                        raise
    539580                    for filter_ in self.filters:
    540                         fallback = filter_(iter(fallback), ctxt)
     581                        fallback = filter_(iter(fallback), ctxt, **vars)
    541582                    for event in fallback:
    542583                        yield event
  • branches/experimental/match-fastpaths/genshi/template/directives.py

    r803 r819  
    2323from genshi.path import Path
    2424from genshi.template.base import TemplateRuntimeError, TemplateSyntaxError, \
    25                                  EXPR, _apply_directives, _ctxt2dict
     25                                 EXPR, _apply_directives, _eval_expr, \
     26                                 _exec_suite
    2627from genshi.template.eval import Expression, Suite, ExpressionASTTransformer, \
    2728                                 _parse
     
    8990    attach = classmethod(attach)
    9091
    91     def __call__(self, stream, ctxt, directives):
     92    def __call__(self, stream, directives, ctxt, **vars):
    9293        """Apply the directive to the given stream.
    9394       
    9495        :param stream: the event stream
    95         :param ctxt: the context data
    9696        :param directives: a list of the remaining directives that should
    9797                           process the stream
     98        :param ctxt: the context data
     99        :param vars: additional variables that should be made available when
     100                     Python code is executed
    98101        """
    99102        raise NotImplementedError
     
    168171    __slots__ = []
    169172
    170     def __call__(self, stream, ctxt, directives):
     173    def __call__(self, stream, directives, ctxt, **vars):
    171174        def _generate():
    172175            kind, (tag, attrib), pos  = stream.next()
    173             attrs = self.expr.evaluate(ctxt)
     176            attrs = _eval_expr(self.expr, ctxt, **vars)
    174177            if attrs:
    175178                if isinstance(attrs, Stream):
     
    187190                yield event
    188191
    189         return _apply_directives(_generate(), ctxt, directives)
     192        return _apply_directives(_generate(), directives, ctxt, **vars)
    190193
    191194
     
    292295    attach = classmethod(attach)
    293296
    294     def __call__(self, stream, ctxt, directives):
     297    def __call__(self, stream, directives, ctxt, **vars):
    295298        stream = list(stream)
    296299
     
    305308                        val = kwargs.pop(name)
    306309                    else:
    307                         val = self.defaults.get(name).evaluate(ctxt)
     310                        val = _eval_expr(self.defaults.get(name), ctxt, **vars)
    308311                    scope[name] = val
    309312            if not self.star_args is None:
     
    312315                scope[self.dstar_args] = kwargs
    313316            ctxt.push(scope)
    314             for event in _apply_directives(stream, ctxt, directives):
     317            for event in _apply_directives(stream, directives, ctxt, **vars):
    315318                yield event
    316319            ctxt.pop()
     
    365368    attach = classmethod(attach)
    366369
    367     def __call__(self, stream, ctxt, directives):
    368         iterable = self.expr.evaluate(ctxt)
     370    def __call__(self, stream, directives, ctxt, **vars):
     371        iterable = _eval_expr(self.expr, ctxt, **vars)
    369372        if iterable is None:
    370373            return
     
    376379            assign(scope, item)
    377380            ctxt.push(scope)
    378             for event in _apply_directives(stream, ctxt, directives):
     381            for event in _apply_directives(stream, directives, ctxt, **vars):
    379382                yield event
    380383            ctxt.pop()
     
    406409    attach = classmethod(attach)
    407410
    408     def __call__(self, stream, ctxt, directives):
    409         if self.expr.evaluate(ctxt):
    410             return _apply_directives(stream, ctxt, directives)
     411    def __call__(self, stream, directives, ctxt, **vars):
     412        value = _eval_expr(self.expr, ctxt, **vars)
     413        if value:
     414            return _apply_directives(stream, directives, ctxt, **vars)
    411415        return []
    412416
     
    441445        hints = []
    442446        if type(value) is dict:
     447            if value.get('buffer', '').lower() == 'false':
     448                hints.append('not_buffered')
    443449            if value.get('once', '').lower() == 'true':
    444450                hints.append('match_once')
     
    450456    attach = classmethod(attach)
    451457
    452     def __call__(self, stream, ctxt, directives):
     458    def __call__(self, stream, directives, ctxt, **vars):
    453459        ctxt._match_set.add((self.path.test(ignore_context=True),
    454460                             self.path, list(stream), self.hints,
     
    532538    __slots__ = []
    533539
    534     def __call__(self, stream, ctxt, directives):
     540    def __call__(self, stream, directives, ctxt, **vars):
    535541        def _generate():
    536             if self.expr.evaluate(ctxt):
     542            if _eval_expr(self.expr, ctxt, **vars):
    537543                stream.next() # skip start tag
    538544                previous = stream.next()
     
    543549                for event in stream:
    544550                    yield event
    545         return _apply_directives(_generate(), ctxt, directives)
     551        return _apply_directives(_generate(), directives, ctxt, **vars)
    546552
    547553    def attach(cls, template, stream, value, namespaces, pos):
     
    601607    attach = classmethod(attach)
    602608
    603     def __call__(self, stream, ctxt, directives):
     609    def __call__(self, stream, directives, ctxt, **vars):
    604610        info = [False, bool(self.expr), None]
    605611        if self.expr:
    606             info[2] = self.expr.evaluate(ctxt)
     612            info[2] = _eval_expr(self.expr, ctxt, **vars)
    607613        ctxt._choice_stack.append(info)
    608         for event in _apply_directives(stream, ctxt, directives):
     614        for event in _apply_directives(stream, directives, ctxt, **vars):
    609615            yield event
    610616        ctxt._choice_stack.pop()
     
    630636    attach = classmethod(attach)
    631637
    632     def __call__(self, stream, ctxt, directives):
     638    def __call__(self, stream, directives, ctxt, **vars):
    633639        info = ctxt._choice_stack and ctxt._choice_stack[-1]
    634640        if not info:
     
    645651            value = info[2]
    646652            if self.expr:
    647                 matched = value == self.expr.evaluate(ctxt)
     653                matched = value == _eval_expr(self.expr, ctxt, **vars)
    648654            else:
    649655                matched = bool(value)
    650656        else:
    651             matched = bool(self.expr.evaluate(ctxt))
     657            matched = bool(_eval_expr(self.expr, ctxt, **vars))
    652658        info[0] = matched
    653659        if not matched:
    654660            return []
    655661
    656         return _apply_directives(stream, ctxt, directives)
     662        return _apply_directives(stream, directives, ctxt, **vars)
    657663
    658664
     
    669675        self.filename = template.filepath
    670676
    671     def __call__(self, stream, ctxt, directives):
     677    def __call__(self, stream, directives, ctxt, **vars):
    672678        info = ctxt._choice_stack and ctxt._choice_stack[-1]
    673679        if not info:
     
    679685        info[0] = True
    680686
    681         return _apply_directives(stream, ctxt, directives)
     687        return _apply_directives(stream, directives, ctxt, **vars)
    682688
    683689
     
    723729    attach = classmethod(attach)
    724730
    725     def __call__(self, stream, ctxt, directives):
    726         frame = {}
    727         ctxt.push(frame)
    728         self.suite.execute(_ctxt2dict(ctxt))
    729         for event in _apply_directives(stream, ctxt, directives):
     731    def __call__(self, stream, directives, ctxt, **vars):
     732        ctxt.push({})
     733        _exec_suite(self.suite, ctxt, **vars)
     734        for event in _apply_directives(stream, directives, ctxt, **vars):
    730735            yield event
    731736        ctxt.pop()
  • branches/experimental/match-fastpaths/genshi/template/eval.py

    r800 r819  
    140140        """
    141141        __traceback_hide__ = 'before_and_this'
    142         _globals = self._globals()
    143         _globals['__data__'] = data
     142        _globals = self._globals(data)
    144143        return eval(self.code, _globals, {'__data__': data})
    145144
     
    162161        """
    163162        __traceback_hide__ = 'before_and_this'
    164         _globals = self._globals()
    165         _globals['__data__'] = data
     163        _globals = self._globals(data)
    166164        exec self.code in _globals, data
    167165
     
    249247    """Abstract base class for variable lookup implementations."""
    250248
    251     def globals(cls):
     249    def globals(cls, data):
    252250        """Construct the globals dictionary to use as the execution context for
    253251        the expression or suite.
    254252        """
    255253        return {
     254            '__data__': data,
    256255            '_lookup_name': cls.lookup_name,
    257256            '_lookup_attr': cls.lookup_attr,
    258257            '_lookup_item': cls.lookup_item,
    259             'UndefinedError': UndefinedError
     258            'UndefinedError': UndefinedError,
    260259        }
    261260    globals = classmethod(globals)
     
    271270    lookup_name = classmethod(lookup_name)
    272271
    273     def lookup_attr(cls, data, obj, key):
     272    def lookup_attr(cls, obj, key):
    274273        __traceback_hide__ = True
    275         val = getattr(obj, key, UNDEFINED)
    276         if val is UNDEFINED:
    277             try:
    278                 val = obj[key]
    279             except (KeyError, TypeError):
    280                 val = cls.undefined(key, owner=obj)
     274        try:
     275            val = getattr(obj, key)
     276        except AttributeError:
     277            if hasattr(obj.__class__, key):
     278                raise
     279            else:
     280                try:
     281                    val = obj[key]
     282                except (KeyError, TypeError):
     283                    val = cls.undefined(key, owner=obj)
    281284        return val
    282285    lookup_attr = classmethod(lookup_attr)
    283286
    284     def lookup_item(cls, data, obj, key):
     287    def lookup_item(cls, obj, key):
    285288        __traceback_hide__ = True
    286289        if len(key) == 1:
     
    755758    def visitGetattr(self, node):
    756759        return ast.CallFunc(ast.Name('_lookup_attr'), [
    757             ast.Name('__data__'), self.visit(node.expr),
     760            self.visit(node.expr),
    758761            ast.Const(node.attrname)
    759762        ])
     
    761764    def visitSubscript(self, node):
    762765        return ast.CallFunc(ast.Name('_lookup_item'), [
    763             ast.Name('__data__'), self.visit(node.expr),
     766            self.visit(node.expr),
    764767            ast.Tuple([self.visit(sub) for sub in node.subs])
    765768        ])
  • branches/experimental/match-fastpaths/genshi/template/loader.py

    r722 r819  
    8383        :param search_path: a list of absolute path names that should be
    8484                            searched for template files, or a string containing
    85                             a single absolute path
     85                            a single absolute path; alternatively, any item on
     86                            the list may be a ''load function'' that is passed
     87                            a filename and returns a file-like object and some
     88                            metadata
    8689        :param auto_reload: whether to check the last modification time of
    8790                            template files, and reload them if they have changed
     
    110113        if self.search_path is None:
    111114            self.search_path = []
    112         elif isinstance(self.search_path, basestring):
     115        elif not isinstance(self.search_path, (list, tuple)):
    113116            self.search_path = [self.search_path]
    114117
     
    131134        """Load the template with the given name.
    132135       
    133         If the `filename` parameter is relative, this method searches the search
    134         path trying to locate a template matching the given name. If the file
    135         name is an absolute path, the search path is ignored.
     136        If the `filename` parameter is relative, this method searches the
     137        search path trying to locate a template matching the given name. If the
     138        file name is an absolute path, the search path is ignored.
    136139       
    137140        If the requested template is not found, a `TemplateNotFound` exception
     
    156159                         ``default_encoding`` of the loader instance
    157160        :return: the loaded `Template` instance
    158         :raises TemplateNotFound: if a template with the given name could not be
    159                                   found
     161        :raises TemplateNotFound: if a template with the given name could not
     162                                  be found
    160163        """
    161164        if cls is None:
    162165            cls = self.default_class
    163         if encoding is None:
    164             encoding = self.default_encoding
    165166        if relative_to and not os.path.isabs(relative_to):
    166167            filename = os.path.join(os.path.dirname(relative_to), filename)
    167168        filename = os.path.normpath(filename)
     169        cachekey = filename
    168170
    169171        self._lock.acquire()
     
    171173            # First check the cache to avoid reparsing the same file
    172174            try:
    173                 tmpl = self._cache[filename]
    174                 if not self.auto_reload or \
    175                         os.path.getmtime(tmpl.filepath) == self._mtime[filename]:
     175                tmpl = self._cache[cachekey]
     176                if not self.auto_reload:
     177                    return tmpl
     178                mtime = self._mtime[cachekey]
     179                if mtime and mtime == os.path.getmtime(tmpl.filepath):
    176180                    return tmpl
    177181            except KeyError, OSError:
     
    191195                dirname = os.path.dirname(relative_to)
    192196                if dirname not in search_path:
    193                     search_path = search_path + [dirname]
     197                    search_path = list(search_path) + [dirname]
    194198                isabs = True
    195199
     
    198202                raise TemplateError('Search path for templates not configured')
    199203
    200             for dirname in search_path:
    201                 filepath = os.path.join(dirname, filename)
     204            for loadfunc in search_path:
     205                if isinstance(loadfunc, basestring):
     206                    loadfunc = directory(loadfunc)
    202207                try:
    203                     fileobj = open(filepath, 'U')
     208                    dirname, filename, fileobj, mtime = loadfunc(filename)
     209                except IOError:
     210                    continue
     211                else:
    204212                    try:
    205213                        if isabs:
     
    207215                            # including template is absolute, make sure the
    208216                            # included template gets an absolute path, too,
    209                             # so that nested include work properly without a
     217                            # so that nested includes work properly without a
    210218                            # search path
    211219                            filename = os.path.join(dirname, filename)
    212220                            dirname = ''
    213                         tmpl = cls(fileobj, basedir=dirname, filename=filename,
    214                                    loader=self, encoding=encoding,
    215                                    lookup=self.variable_lookup,
    216                                    allow_exec=self.allow_exec)
     221                        tmpl = self.instantiate(cls, fileobj, dirname,
     222                                                filename, encoding=encoding)
    217223                        if self.callback:
    218224                            self.callback(tmpl)
    219                         self._cache[filename] = tmpl
    220                         self._mtime[filename] = os.path.getmtime(filepath)
     225                        self._cache[cachekey] = tmpl
     226                        self._mtime[cachekey] = mtime
    221227                    finally:
    222                         fileobj.close()
     228                        if hasattr(fileobj, 'close'):
     229                            fileobj.close()
    223230                    return tmpl
    224                 except IOError:
    225                     continue
    226231
    227232            raise TemplateNotFound(filename, search_path)
     
    229234        finally:
    230235            self._lock.release()
     236
     237    def instantiate(self, cls, fileobj, dirname, filename, encoding=None):
     238        """Instantiate and return the `Template` object based on the given
     239        class and parameters.
     240       
     241        This function is intended for subclasses to override if they need to
     242        implement special template instantiation logic. Code that just uses
     243        the `TemplateLoader` should use the `load` method instead.
     244       
     245        :param cls: the class of the template object to instantiate
     246        :param fileobj: a readable file-like object containing the template
     247                        source
     248        :param dirname: the name of the base directory containing the template
     249                        file
     250        :param filename: the name of the template file, relative to the given
     251                         base directory
     252        :param encoding: the encoding of the template to load; defaults to the
     253                         ``default_encoding`` of the loader instance
     254        :return: the loaded `Template` instance
     255        :rtype: `Template`
     256        """
     257        if encoding is None:
     258            encoding = self.default_encoding
     259        return cls(fileobj, basedir=dirname, filename=filename, loader=self,
     260                   encoding=encoding, lookup=self.variable_lookup,
     261                   allow_exec=self.allow_exec)
     262
     263    def directory(path):
     264        """Loader factory for loading templates from a local directory.
     265       
     266        :param path: the path to the local directory containing the templates
     267        :return: the loader function to load templates from the given directory
     268        :rtype: ``function``
     269        """
     270        def _load_from_directory(filename):
     271            filepath = os.path.join(path, filename)
     272            fileobj = open(filepath, 'U')
     273            return path, filename, fileobj, os.path.getmtime(filepath)
     274        return _load_from_directory
     275    directory = staticmethod(directory)
     276
     277    def package(name, path):
     278        """Loader factory for loading templates from egg package data.
     279       
     280        :param name: the name of the package containing the resources
     281        :param path: the path inside the package data
     282        :return: the loader function to load templates from the given package
     283        :rtype: ``function``
     284        """
     285        from pkg_resources import resource_stream
     286        def _load_from_package(filename):
     287            filepath = os.path.join(path, filename)
     288            return path, filename, resource_stream(name, filepath), None
     289        return _load_from_package
     290    package = staticmethod(package)
     291
     292    def prefixed(**delegates):
     293        """Factory for a load function that delegates to other loaders
     294        depending on the prefix of the requested template path.
     295       
     296        The prefix is stripped from the filename when passing on the load
     297        request to the delegate.
     298       
     299        >>> load = prefixed(
     300        ...     app1 = lambda filename: ('app1', filename, None, None),
     301        ...     app2 = lambda filename: ('app2', filename, None, None)
     302        ... )
     303        >>> print load('app1/foo.html')
     304        ('', 'app1/foo.html', None, None)
     305        >>> print load('app2/bar.html')
     306        ('', 'app2/bar.html', None, None)
     307
     308        :param delegates: mapping of path prefixes to loader functions
     309        :return: the loader function
     310        :rtype: ``function``
     311        """
     312        def _dispatch_by_prefix(filename):
     313            for prefix, delegate in delegates.items():
     314                if filename.startswith(prefix):
     315                    if isinstance(delegate, basestring):
     316                        delegate = directory(delegate)
     317                    path, _, fileobj, mtime = delegate(
     318                        filename[len(prefix):].lstrip('/\\')
     319                    )
     320                    dirname = path[len(prefix):].rstrip('/\\')
     321                    return dirname, filename, fileobj, mtime
     322            raise TemplateNotFound(filename, delegates.keys())
     323        return _dispatch_by_prefix
     324    prefixed = staticmethod(prefixed)
     325
     326directory = TemplateLoader.directory
     327package = TemplateLoader.package
     328prefixed = TemplateLoader.prefixed
  • branches/experimental/match-fastpaths/genshi/template/markup.py

    r806 r819  
    227227        return streams[0]
    228228
    229     def _match(self, stream, ctxt, match_set=None):
     229    def _match(self, stream, ctxt, match_set=None, **vars):
    230230        """Internal stream filter that applies any defined match templates
    231231        to the stream.
     
    264264                    match_template
    265265                if test(event, namespaces, ctxt) is True:
     266                    post_match_templates = \
     267                        match_set.after_template(match_template)
     268                   
    266269                    if 'match_once' in hints:
     270
     271                        # need to save this before we nuke
     272                        # match_template from match_set
     273                        pre_match_templates = \
     274                            match_set.before_template(match_template, False)
     275                       
     276                        # forcibly remove this template from this and
     277                        # all child match sets
    267278                        match_set.remove(match_template)
    268279                        del match_candidates[idx]
    269280                        idx -= 1
     281                    else:
     282                        inclusive = True
     283                        if 'not_recursive' in hints:
     284                            inclusive=False
     285                        pre_match_templates = match_set.before_template(match_template, inclusive)
    270286
    271287                    # Let the remaining match templates know about the event so
     
    277293                    # corresponding to this start event is encountered
    278294                    inner = _strip(stream)
    279                     if 'match_once' not in hints \
    280                             and 'not_recursive' not in hints:
    281                         inner = self._match(inner, ctxt,
    282                                             MatchSet.single_match(match_template))
    283                     content = list(self._include(chain([event], inner, tail),
    284                                                  ctxt))
     295                    if pre_match_templates:
     296                        inner = self._match(inner, ctxt, pre_match_templates)
     297                    content = self._include(chain([event], inner, tail), ctxt)
     298                    if 'not_buffered' not in hints:
     299                        content = list(content)
    285300
    286301                    # Now tell all the match templates about the
    287302                    # END event (tail[0])
    288                     for test in [mt[0] for mt in match_candidates]:
    289                         test(tail[0], namespaces, ctxt, updateonly=True)
     303                    if tail:
     304                        for test in [mt[0] for mt in match_candidates]:
     305                            test(tail[0], namespaces, ctxt, updateonly=True)
    290306
    291307                    # Make the select() function available in the body of the
     
    293309                    def select(path):
    294310                        return Stream(content).select(path, namespaces, ctxt)
    295                     ctxt.push(dict(select=select))
     311                    vars = dict(select=select)
    296312
    297313                    # Recursively process the output
    298                     template = _apply_directives(template, ctxt, directives)
    299                     remaining = match_set
    300                     if 'match_once' not in hints:
    301                         # match has not been removed, so we need an
    302                         # exclusion matchset
    303                         remaining = match_set.with_exclusion(match_template)
    304                        
    305                     body = self._exec(self._eval(self._flatten(template, ctxt),
    306                                                  ctxt), ctxt)
    307                     for event in self._match(body, ctxt, remaining):
     314                    template = _apply_directives(template, directives, ctxt,
     315                                                 **vars)
     316                    for event in self._match(
     317                            self._exec(
     318                                self._eval(
     319                                    self._flatten(template, ctxt, **vars),
     320                                    ctxt, **vars),
     321                                ctxt, **vars),
     322                            ctxt, post_match_templates,
     323                            **vars):
    308324                        yield event
    309325
    310                     ctxt.pop()
    311326                    break
    312327
  • branches/experimental/match-fastpaths/genshi/template/match.py

    r817 r819  
    33
    44from copy import copy
     5from itertools import ifilter
    56
    67def is_simple_path(path):
     
    3637    If the path is more complex like "xyz[k=z]" then then that match
    3738    will always be returned by ``find_matches``.  """
    38     def __init__(self, parent=None, exclude=None):
     39    def __init__(self, parent=None,
     40                 min_index=None,
     41                 max_index=None):
    3942        """
    4043        If a parent is given, it means this is a wrapper around another
    4144        set.
    4245       
    43         If exclude is given, it means include everything in the
    44         parent, but exclude a specific match template.
    4546        """
    4647        self.parent = parent
    47 
    48         self.current_index = 0
    4948
    5049        if parent is None:
     
    5453            self.match_order = {}
    5554           
     55            self.min_index = None
     56            self.max_index = None
     57
    5658            # tag_templates are match templates whose path are simply
    5759            # a tag, like "body" or "img"
     
    6264            self.other_templates = []
    6365
    64             # exclude is a list of templates to ignore when iterating
    65             # through templates
    66             self.exclude = []
    67             if exclude is not None:
    68                 self.exclude.append(exclude)
    6966        else:
    7067            # We have a parent: Just copy references to member
    71             # variables in parent so that there's no performance loss,
    72             # but make our own exclusion set, so we don't have to
    73             # chain exclusions across a chain of MatchSets
     68            # variables in parent so that there's no performance loss
     69            self.max_index = parent.max_index
     70            self.min_index = parent.min_index
    7471            self.match_order = parent.match_order
    7572            self.tag_templates = parent.tag_templates
    7673            self.other_templates = parent.other_templates
    77        
    78             self.exclude = copy(parent.exclude)
    79             if exclude is not None:
    80                 self.exclude.append(exclude)
     74
     75        if max_index is not None:
     76            assert self.max_index is None or max_index <= self.max_index
     77            self.max_index = max_index
     78
     79        if min_index is not None:
     80            assert self.min_index is None or min_index > self.min_index
     81            self.min_index = min_index
     82       
    8183   
    8284    def add(self, match_template):
     
    9294        path = match_template[1]
    9395
    94         self.current_index += 1
    9596        if is_simple_path(path):
    9697            # special cache of tag
     
    134135    single_match = classmethod(single_match)
    135136
    136     def with_exclusion(self, exclude):
    137         """
    138         Factory for creating a MatchSet based on another MatchSet, but
    139         with certain templates excluded
    140         """
    141         cls = self.__class__
    142         new_match_set = cls(parent=self, exclude=exclude)
    143         return new_match_set
    144            
     137    def before_template(self, match_template, inclusive):
     138        cls = type(self)
     139        max_index = self.match_order[id(match_template)]
     140        if not inclusive:
     141            max_index -= 1
     142        return cls(parent=self, max_index=max_index)
     143   
     144    def after_template(self, match_template):
     145        """
     146        Factory for creating a MatchSet that only matches templates after
     147        the given match
     148        """
     149        cls = type(self)
     150        min_index = self.match_order[id(match_template)] + 1
     151        return cls(parent=self, min_index=min_index)
     152   
    145153    def find_raw_matches(self, event):
    146154        """ Return a list of all valid templates that can be used for the
     
    168176
    169177        # remove exclusions
    170         matches = filter(lambda template: template not in self.exclude,
    171                          self.find_raw_matches(event))
     178        def can_match(template):
     179            # make sure that
     180            if (self.min_index is not None and
     181                self.match_order[id(template)] < self.min_index):
     182                return False
     183
     184            if (self.max_index is not None and
     185                self.match_order[id(template)] > self.max_index):
     186                return False
     187
     188            return True
     189       
     190        matches = ifilter(can_match,
     191                          self.find_raw_matches(event))
    172192
    173193        # sort the results according to the order they were added
     
    178198        allow this to behave as a list
    179199        """
     200
     201        # this is easy - before the first element there is nothing
     202        if self.max_index == -1:
     203            return False
     204
     205        # this isn't always right because match_order may shrink, but
     206        # you'll never get a false-negative
     207        if self.min_index == len(self.match_order):
     208            return False
     209
     210        # check for a range that is completely constrained
     211        if self.min_index is not None and self.max_index is not None:
     212            if self.min_index >= self.max_index:
     213                return False
     214           
    180215        return bool(self.tag_templates or self.other_templates)
    181216
     
    185220            parent = ": child of 0x%x" % id(self.parent)
    186221
    187         exclude = ""
    188         if self.exclude:
    189             exclude = " / excluding %d items" % len(self.exclude)
    190            
    191         return "<MatchSet 0x%x %d tag templates, %d other templates%s%s>" % (id(self), len(self.tag_templates), len(self.other_templates), parent, exclude)
     222        return "<MatchSet 0x%x %d tag templates, %d other templates, range=[%s:%s]%s>" % (
     223            id(self), len(self.tag_templates), len(self.other_templates),
     224            self.min_index, self.max_index,
     225            parent)
  • branches/experimental/match-fastpaths/genshi/template/tests/__init__.py

    r538 r819  
    1717def suite():
    1818    from genshi.template.tests import base, directives, eval, interpolation, \
    19                                       loader, markup, plugin, text
     19                                      loader, markup, plugin, text, match
    2020    suite = unittest.TestSuite()
    2121    suite.addTest(base.suite())
     
    2727    suite.addTest(plugin.suite())
    2828    suite.addTest(text.suite())
     29    suite.addTest(match.suite())
    2930    return suite
    3031
  • branches/experimental/match-fastpaths/genshi/template/tests/directives.py

    r773 r819  
    631631          </body>
    632632        </html>""", str(tmpl.generate()))
     633
     634    def test_recursive_match_3(self):
     635        tmpl = MarkupTemplate("""<test xmlns:py="http://genshi.edgewall.org/">
     636          <py:match path="b[@type='bullet']">
     637            <bullet>${select('*|text()')}</bullet>
     638          </py:match>
     639          <py:match path="group[@type='bullet']">
     640            <ul>${select('*')}</ul>
     641          </py:match>
     642          <py:match path="b">
     643            <generic>${select('*|text()')}</generic>
     644          </py:match>
     645
     646          <b>
     647            <group type="bullet">
     648              <b type="bullet">1</b>
     649              <b type="bullet">2</b>
     650            </group>
     651          </b>
     652        </test>
     653        """)
     654        self.assertEqual("""<test>
     655            <generic>
     656            <ul><bullet>1</bullet><bullet>2</bullet></ul>
     657          </generic>
     658        </test>""", str(tmpl.generate()))
    633659
    634660    def test_not_match_self(self):
  • branches/experimental/match-fastpaths/genshi/template/tests/eval.py

    r798 r819  
    343343    def test_getattr_exception(self):
    344344        class Something(object):
    345             def prop(self):
     345            def prop_a(self):
    346346                raise NotImplementedError
    347             prop = property(prop)
     347            prop_a = property(prop_a)
     348            def prop_b(self):
     349                raise AttributeError
     350            prop_b = property(prop_b)
    348351        self.assertRaises(NotImplementedError,
    349                           Expression('s.prop').evaluate, {'s': Something()})
     352                          Expression('s.prop_a').evaluate, {'s': Something()})
     353        self.assertRaises(AttributeError,
     354                          Expression('s.prop_b').evaluate, {'s': Something()})
    350355
    351356    def test_getitem_undefined_string(self):
  • branches/experimental/match-fastpaths/genshi/template/tests/loader.py

    r531 r819  
    105105            </html>""", tmpl.generate().render())
    106106
     107    def test_relative_include_samesubdir(self):
     108        file1 = open(os.path.join(self.dirname, 'tmpl1.html'), 'w')
     109        try:
     110            file1.write("""<div>Included tmpl1.html</div>""")
     111        finally:
     112            file1.close()
     113
     114        os.mkdir(os.path.join(self.dirname, 'sub'))
     115        file2 = open(os.path.join(self.dirname, 'sub', 'tmpl1.html'), 'w')
     116        try:
     117            file2.write("""<div>Included sub/tmpl1.html</div>""")
     118        finally:
     119            file2.close()
     120
     121        file3 = open(os.path.join(self.dirname, 'sub', 'tmpl2.html'), 'w')
     122        try:
     123            file3.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude">
     124              <xi:include href="tmpl1.html" />
     125            </html>""")
     126        finally:
     127            file3.close()
     128
     129        loader = TemplateLoader([self.dirname])
     130        tmpl = loader.load('sub/tmpl2.html')
     131        self.assertEqual("""<html>
     132              <div>Included sub/tmpl1.html</div>
     133            </html>""", tmpl.generate().render())
     134
    107135    def test_relative_include_without_search_path(self):
    108136        file1 = open(os.path.join(self.dirname, 'tmpl1.html'), 'w')
     
    172200          <div>Included</div>
    173201        </html>""", tmpl2.generate().render())
     202
     203    def test_relative_absolute_template_preferred(self):
     204        file1 = open(os.path.join(self.dirname, 'tmpl1.html'), 'w')
     205        try:
     206            file1.write("""<div>Included</div>""")
     207        finally:
     208            file1.close()
     209
     210        os.mkdir(os.path.join(self.dirname, 'sub'))
     211        file2 = open(os.path.join(self.dirname, 'sub', 'tmpl1.html'), 'w')
     212        try:
     213            file2.write("""<div>Included from sub</div>""")
     214        finally:
     215            file2.close()
     216
     217        file3 = open(os.path.join(self.dirname, 'sub', 'tmpl2.html'), 'w')
     218        try:
     219            file3.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude">
     220              <xi:include href="tmpl1.html" />
     221            </html>""")
     222        finally:
     223            file3.close()
     224
     225        loader = TemplateLoader()
     226        tmpl = loader.load(os.path.abspath(os.path.join(self.dirname, 'sub',
     227                                                        'tmpl2.html')))
     228        self.assertEqual("""<html>
     229              <div>Included from sub</div>
     230            </html>""", tmpl.generate().render())
     231
     232    def test_abspath_caching(self):
     233        abspath = os.path.join(self.dirname, 'abs')
     234        os.mkdir(abspath)
     235        file1 = open(os.path.join(abspath, 'tmpl1.html'), 'w')
     236        try:
     237            file1.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude">
     238              <xi:include href="tmpl2.html" />
     239            </html>""")
     240        finally:
     241            file1.close()
     242
     243        file2 = open(os.path.join(abspath, 'tmpl2.html'), 'w')
     244        try:
     245            file2.write("""<div>Included from abspath.</div>""")
     246        finally:
     247            file2.close()
     248
     249        searchpath = os.path.join(self.dirname, 'searchpath')
     250        os.mkdir(searchpath)
     251        file3 = open(os.path.join(searchpath, 'tmpl2.html'), 'w')
     252        try:
     253            file3.write("""<div>Included from searchpath.</div>""")
     254        finally:
     255            file3.close()
     256
     257        loader = TemplateLoader(searchpath)
     258        tmpl1 = loader.load(os.path.join(abspath, 'tmpl1.html'))
     259        self.assertEqual("""<html>
     260              <div>Included from searchpath.</div>
     261            </html>""", tmpl1.generate().render())
     262        assert 'tmpl2.html' in loader._cache
    174263
    175264    def test_load_with_default_encoding(self):
     
    220309            </html>""", tmpl.generate().render())
    221310
     311    def test_prefix_delegation_to_directories(self):
     312        """
     313        Test prefix delegation with the following layout:
     314       
     315        templates/foo.html
     316        sub1/templates/tmpl1.html
     317        sub2/templates/tmpl2.html
     318       
     319        Where sub1 and sub2 are prefixes, and both tmpl1.html and tmpl2.html
     320        incldue foo.html.
     321        """
     322        dir1 = os.path.join(self.dirname, 'templates')
     323        os.mkdir(dir1)
     324        file1 = open(os.path.join(dir1, 'foo.html'), 'w')
     325        try:
     326            file1.write("""<div>Included foo</div>""")
     327        finally:
     328            file1.close()
     329
     330        dir2 = os.path.join(self.dirname, 'sub1', 'templates')
     331        os.makedirs(dir2)
     332        file2 = open(os.path.join(dir2, 'tmpl1.html'), 'w')
     333        try:
     334            file2.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude">
     335              <xi:include href="../foo.html" /> from sub1
     336            </html>""")
     337        finally:
     338            file2.close()
     339
     340        dir3 = os.path.join(self.dirname, 'sub2', 'templates')
     341        os.makedirs(dir3)
     342        file3 = open(os.path.join(dir3, 'tmpl2.html'), 'w')
     343        try:
     344            file3.write("""<div>tmpl2</div>""")
     345        finally:
     346            file3.close()
     347
     348        loader = TemplateLoader([dir1, TemplateLoader.prefixed(
     349            sub1 = os.path.join(dir2),
     350            sub2 = os.path.join(dir3)
     351        )])
     352        tmpl = loader.load('sub1/tmpl1.html')
     353        self.assertEqual("""<html>
     354              <div>Included foo</div> from sub1
     355            </html>""", tmpl.generate().render())
     356
     357    def test_prefix_delegation_to_directories_with_subdirs(self):
     358        """
     359        Test prefix delegation with the following layout:
     360       
     361        templates/foo.html
     362        sub1/templates/tmpl1.html
     363        sub1/templates/tmpl2.html
     364        sub1/templates/bar/tmpl3.html
     365       
     366        Where sub1 is a prefix, and tmpl1.html includes all the others.
     367        """
     368        dir1 = os.path.join(self.dirname, 'templates')
     369        os.mkdir(dir1)
     370        file1 = open(os.path.join(dir1, 'foo.html'), 'w')
     371        try:
     372            file1.write("""<div>Included foo</div>""")
     373        finally:
     374            file1.close()
     375
     376        dir2 = os.path.join(self.dirname, 'sub1', 'templates')
     377        os.makedirs(dir2)
     378        file2 = open(os.path.join(dir2, 'tmpl1.html'), 'w')
     379        try:
     380            file2.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude">
     381              <xi:include href="../foo.html" /> from sub1
     382              <xi:include href="tmpl2.html" /> from sub1
     383              <xi:include href="bar/tmpl3.html" /> from sub1
     384            </html>""")
     385        finally:
     386            file2.close()
     387
     388        file3 = open(os.path.join(dir2, 'tmpl2.html'), 'w')
     389        try:
     390            file3.write("""<div>tmpl2</div>""")
     391        finally:
     392            file3.close()
     393
     394        dir3 = os.path.join(self.dirname, 'sub1', 'templates', 'bar')
     395        os.makedirs(dir3)
     396        file4 = open(os.path.join(dir3, 'tmpl3.html'), 'w')
     397        try:
     398            file4.write("""<div>bar/tmpl3</div>""")
     399        finally:
     400            file4.close()
     401
     402        loader = TemplateLoader([dir1, TemplateLoader.prefixed(
     403            sub1 = os.path.join(dir2),
     404            sub2 = os.path.join(dir3)
     405        )])
     406        tmpl = loader.load('sub1/tmpl1.html')
     407        self.assertEqual("""<html>
     408              <div>Included foo</div> from sub1
     409              <div>tmpl2</div> from sub1
     410              <div>bar/tmpl3</div> from sub1
     411            </html>""", tmpl.generate().render())
     412
    222413
    223414def suite():
  • branches/experimental/match-fastpaths/genshi/template/tests/markup.py

    r772 r819  
    612612        </html>""", tmpl.generate().render())
    613613
     614    def test_with_in_match(self):
     615        xml = ("""<html xmlns:py="http://genshi.edgewall.org/">
     616          <py:match path="body/p">
     617            <h1>${select('text()')}</h1>
     618            ${select('.')}
     619          </py:match>
     620          <body><p py:with="foo='bar'">${foo}</p></body>
     621        </html>""")
     622        tmpl = MarkupTemplate(xml, filename='test.html')
     623        self.assertEqual("""<html>
     624          <body>
     625            <h1>bar</h1>
     626            <p>bar</p>
     627          </body>
     628        </html>""", tmpl.generate().render())
     629
    614630    def test_nested_include_matches(self):
    615631        # See ticket #157
  • branches/experimental/match-fastpaths/genshi/template/tests/text.py

    r745 r819  
    233233----- Included data above this line -----""", tmpl.generate().render())
    234234
     235    def test_include_expr(self):
     236         file1 = open(os.path.join(self.dirname, 'tmpl1.txt'), 'w')
     237         try:
     238             file1.write("Included")
     239         finally:
     240             file1.close()
     241 
     242         file2 = open(os.path.join(self.dirname, 'tmpl2.txt'), 'w')
     243         try:
     244             file2.write("""----- Included data below this line -----
     245    {% include ${'%s.txt' % ('tmpl1',)} %}
     246    ----- Included data above this line -----""")
     247         finally:
     248             file2.close()
     249
     250         loader = TemplateLoader([self.dirname])
     251         tmpl = loader.load('tmpl2.txt', cls=NewTextTemplate)
     252         self.assertEqual("""----- Included data below this line -----
     253    Included
     254    ----- Included data above this line -----""", tmpl.generate().render())
     255
    235256
    236257def suite():
  • branches/experimental/match-fastpaths/genshi/template/text.py

    r745 r819  
    2929import re
    3030
     31from genshi.core import TEXT
    3132from genshi.template.base import BadDirectiveError, Template, \
    3233                                 TemplateSyntaxError, EXEC, INCLUDE, SUB
    3334from genshi.template.eval import Suite
    3435from genshi.template.directives import *
    35 from genshi.template.directives import Directive, _apply_directives
     36from genshi.template.directives import Directive
    3637from genshi.template.interpolation import interpolate
    3738
     
    189190            if command == 'include':
    190191                pos = (self.filename, lineno, 0)
    191                 stream.append((INCLUDE, (value.strip(), None, []), pos))
     192                value = list(interpolate(value, self.basedir, self.filename,
     193                                         lineno, 0, lookup=self.lookup))
     194                if len(value) == 1 and value[0][0] is TEXT:
     195                    value = value[0][1]
     196                stream.append((INCLUDE, (value, None, []), pos))
    192197
    193198            elif command == 'python':
  • branches/experimental/match-fastpaths/genshi/tests/core.py

    r782 r819  
    1515import pickle
    1616from StringIO import StringIO
     17try:
     18    from cStringIO import StringIO as cStringIO
     19except ImportError:
     20    cStringIO = StringIO
    1721import unittest
    1822
     
    3539        xml = XML('<li>Über uns</li>')
    3640        self.assertEqual('<li>&#220;ber uns</li>', xml.render(encoding='ascii'))
     41
     42    def test_render_output_stream_utf8(self):
     43        xml = XML('<li>Über uns</li>')
     44        strio = cStringIO()
     45        self.assertEqual(None, xml.render(out=strio))
     46        self.assertEqual('<li>Über uns</li>', strio.getvalue())
     47
     48    def test_render_output_stream_unicode(self):
     49        xml = XML('<li>Über uns</li>')
     50        strio = StringIO()
     51        self.assertEqual(None, xml.render(encoding=None, out=strio))
     52        self.assertEqual(u'<li>Über uns</li>', strio.getvalue())
    3753
    3854    def test_pickle(self):
  • branches/experimental/match-fastpaths/genshi/tests/output.py

    r787 r819  
    229229        text = '<foo xml:space="preserve"> Do not mess  \n\n with me </foo>'
    230230        output = XML(text).render(XHTMLSerializer)
    231         self.assertEqual(text, output)
     231        self.assertEqual('<foo> Do not mess  \n\n with me </foo>', output)
    232232
    233233    def test_empty_script(self):
Note: See TracChangeset for help on using the changeset viewer.