[z3-checkins] r56144 - in z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance: . tests

ianb at codespeak.net ianb at codespeak.net
Fri Jun 27 23:00:32 CEST 2008


Author: ianb
Date: Fri Jun 27 23:00:30 2008
New Revision: 56144

Added:
   z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/middleware.py   (contents, props changed)
Modified:
   z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/log.py
   z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/pagematch.py
   z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/rules.py
   z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/ruleset.py
   z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/stringmatch.py
   z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/test_middleware.txt
Log:
Mostly got the middleware working

Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/log.py
==============================================================================
--- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/log.py	(original)
+++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/log.py	Fri Jun 27 23:00:30 2008
@@ -7,16 +7,19 @@
 """
 
 import logging
+from lxml.etree import tostring, _Element
 
 class SavingLogger(object):
     """
     Logger that saves all its messages locally.
     """
-    def __init__(self, description=True):
+    def __init__(self, req, description=True):
         self.messages = []
         if description:
             self.descriptions = []
             self.describe = self.add_description
+        else:
+            self.describe = None
     def add_description(self, msg):
         self.descriptions.append(msg)
     def message(self, level, el, msg, *args, **kw):
@@ -25,6 +28,7 @@
         elif kw:
             msg = msg % kw
         self.messages.append((level, el, msg))
+        return msg
     def debug(self, el, msg, *args, **kw):
         self.message(logging.DEBUG, el, msg, *args, **kw)
     def info(self, el, msg, *args, **kw):
@@ -38,3 +42,23 @@
         self.message(logging.ERROR, el, msg, *args, **kw)
     def fatal(self, el, msg, *args, **kw):
         self.message(logging.FATAL, el, msg, *args, **kw)
+
+class PrintingLogger(SavingLogger):
+
+    def __init__(self, req, description=True, print_level=logging.DEBUG):
+        super(PrintingLogger, self).__init__(req, description=description)
+        self.print_level = print_level
+
+    def add_description(self, msg):
+        print 'description:', msg
+        super(PrintingLogger, self).add_description(msg)
+
+    def message(self, level, el, msg, *args, **kw):
+        msg = super(PrintingLogger, self).message(level, el, msg, *args, **kw)
+        if level >= self.print_level:
+            if isinstance(el, _Element):
+                s = tostring(el)
+            else:
+                s = str(el)
+            print '%s (%s)' % (msg, s)
+        return msg

Added: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/middleware.py
==============================================================================
--- (empty file)
+++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/middleware.py	Fri Jun 27 23:00:30 2008
@@ -0,0 +1,61 @@
+from webob import Request
+from deliverance.log import SavingLogger
+import urllib2
+
+class DeliveranceMiddleware(object):
+
+    def __init__(self, app, rule_getter, log_factory=SavingLogger, log_factory_kw={}):
+        self.app = app
+        self.rule_getter = rule_getter
+        self.log_factory = log_factory
+        self.log_factory_kw = log_factory_kw
+
+    def __call__(self, environ, start_response):
+        ## FIXME: copy_get?:
+        orig_req = Request(environ.copy())
+        req = Request(environ)
+        log = self.log_factory(req, **self.log_factory_kw)
+        resp = req.get_response(self.app)
+        ## FIXME: also XHTML?
+        if resp.content_type != 'text/html':
+            return resp(environ, start_response)
+        def get_resource(url):
+            assert url is not None
+            ## FIXME: should this return a webob.Response object?
+            if url.startswith(orig_req.application_url + '/'):
+                subreq = orig_req.copy_get()
+                subreq.environ['deliverance.subrequest_original_environ'] = orig_req.environ
+                new_path_info = url[len(orig_req.application_url):]
+                assert new_path_info.startswith('/')
+                subreq.path_info = new_path_info
+                subresp = subreq.get_response(self.app)
+                ## FIXME: error if not HTML?
+                ## FIXME: handle redirects?
+                ## FIXME: handle non-200?
+                return subresp.body
+            else:
+                ## FIXME: pluggable subrequest handler?
+                f = urllib2.urlopen(url)
+                body = f.read()
+                f.close()
+                return body
+        rule_set = self.rule_getter(get_resource, self.app, orig_req)
+        resp = rule_set.apply_rules(req, resp, get_resource, log)
+        return resp(environ, start_response)
+
+class SubrequestRuleGetter(object):
+
+    def __init__(self, url):
+        self.url = url
+    def __call__(self, get_resource, app, orig_req):
+        from deliverance.ruleset import RuleSet
+        from lxml.etree import XML, XMLSyntaxError
+        import urlparse
+        url = urlparse.urljoin(orig_req.url, self.url)
+        doc_text = get_resource(url)
+        try:
+            doc = XML(doc_text, base_url=url)
+        except XMLSyntaxError, e:
+            raise 'Invalid syntax in %s: %s' % (url, e)
+        assert doc.tag == 'ruleset', 'Bad rule tag <%s> in document %s' % (doc.tag, url)
+        return RuleSet.parse_xml(doc, url)

Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/pagematch.py
==============================================================================
--- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/pagematch.py	(original)
+++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/pagematch.py	Fri Jun 27 23:00:30 2008
@@ -4,6 +4,7 @@
 
 from deliverance.stringmatch import compile_matcher, compile_header_matcher
 from deliverance.util.converters import asbool, html_quote
+from deliverance.rules import AbortTheme
 
 __all__ = ['MatchSyntaxError', 'Match']
 
@@ -81,7 +82,8 @@
 
     def __unicode__(self):
         parts = [u'<match']
-        parts.append(u'class="%s"' % html_quote(' '.join(self.classes)))
+        if self.classes:
+            parts.append(u'class="%s"' % html_quote(' '.join(self.classes)))
         for attr, value in [
             ('path', self.path),
             ('domain', self.domain),
@@ -153,6 +155,9 @@
     results = []
     for matcher in matchers:
         if matcher(request, response_headers, log):
+            if matcher.abort:
+                log.debug(matcher, '<match> matched request, aborting')
+                raise AbortTheme('<match> matched request, aborting')
             log.debug(matcher, '<match> matched request, adding classes %s',
                       ', '.join(matcher.classes))
             for item in matcher.classes:

Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/rules.py
==============================================================================
--- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/rules.py	(original)
+++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/rules.py	Fri Jun 27 23:00:30 2008
@@ -30,10 +30,11 @@
     This represents everything in a <rule></rule> section.
     """
 
-    def __init__(self, classes, actions, theme, source_location):
+    def __init__(self, classes, actions, theme, suppress_standard, source_location):
         self.classes = classes
         self._actions = actions
         self.theme = theme
+        self.suppress_standard = suppress_standard
         self.source_location = source_location
 
     @classmethod
@@ -47,15 +48,18 @@
             classes = ['default']
         theme = None
         actions = []
+        suppress_standard = asbool(el.get('suppress-standard'))
         for el in el.iterchildren():
             if el.tag == 'theme':
                 ## FIXME: error if more than one theme
                 ## FIXME: error if no href
                 theme = el.get('href')
                 continue
+            if el.tag is etree.Comment:
+                continue
             action = parse_action(el, source_location)
             actions.append(action)
-        return cls(classes, actions, theme, source_location)
+        return cls(classes, actions, theme, suppress_standard, source_location)
 
     def apply(self, content_doc, theme_doc, resource_fetcher, log):
         """
@@ -112,29 +116,30 @@
                 bad_options = self._many_allowed
         else:
             if value not in self._no_allowed:
-                vad_options = self._no_allowed
+                bad_options = self._no_allowed
         if bad_options:
             raise RuleSyntaxError(
                 'The attribute %s="%s" should have a value of one of: %s'
                 % (name, value, ', '.join(v for v in bad_options if v)))
+        if not value:
+            value = 'warn'
+        if name in ('nocontent', 'notheme'):
+            return value
+        # Must be manytheme/manycontent, which is (error_handler, fallback)
         if value and ':' in value:
             value = tuple(value.split(':', 1))
         elif value == 'first':
             value = ('ignore', 'first')
         elif value == 'last':
             value = ('ignore', 'last')
-        if name in ('manytheme', 'manycontent'):
-            if value == 'ignore':
-                value = ('ignore', 'first')
-            elif value == 'warn' or not value:
-                value = ('warn', 'first')
-            elif value == 'abort':
-                value = ('abort', None)
-        elif not value:
-            value = ('warn', None)
-        if isinstance(value, basestring):
-            value = (value, None)
-        assert isinstance(value, tuple), 'Bad value: %r' % value
+        if value == 'ignore':
+            value = ('ignore', 'first')
+        elif value == 'warn':
+            value = ('warn', 'first')
+        elif value == 'abort':
+            value = ('abort', None)
+        else:
+            assert 0, "Unexpected value: %r" % value
         return value
 
     def format_error(self, attr, value):
@@ -143,13 +148,16 @@
         back into ``attribute="value"``
         """
         if attr in ('manytheme', 'manycontent'):
+            assert isinstance(value, tuple), (
+                "Unexpected value: %r (for attribute %s)" % (
+                    value, attr))
             handler, pos = value
             if pos == 'last':
                 text = '%s:%s' % (handler, pos)
             else:
                 text = handler
         else:
-            text = value[0]
+            text = value
             if text == 'warn':
                 return None
         return '%s="%s"' % (attr, html_quote(text))
@@ -214,19 +222,26 @@
             parts.append('href="%s"' % html_quote(self.content_href))
         if self.move_supported and not getattr(self, 'move', False):
             parts.append('move="1"')
-        for attr in 'nocontent', 'manycontent':
-            value = getattr(self, 'nocontent', ('warn', None))
-            if value != ('warn', None):
-                parts.append(self.format_error(attr, value))
+        v = getattr(self, 'nocontent', 'warn')
+        if v != 'warn':
+            parts.append(self.format_error('nocontent', v))
+        v = getattr(self, 'manycontent', ('warn', None))
+        if v != ('warn', None):
+            parts.append(self.format_error('manycontent', v))
         if getattr(self, 'theme', None):
             parts.append('theme="%s"' % html_quote(self.theme))
-        for attr in 'notheme', 'manytheme':
-            value = getattr(self, 'nocontent', ('warn', None))
-            if value != ('warn', None):
-                parts.append(self.format_error(attr, value))
+        v = getattr(self, 'notheme', 'warn')
+        if v != 'warn':
+            parts.append(self.format_error('notheme', v))
+        v = getattr(self, 'manytheme', ('warn', None))
+        if v != ('warn', None):
+            parts.append(self.format_error('manytheme', v))
         ## FIXME: add source_location
         return ' '.join(parts) + ' />'
 
+    def __str__(self):
+        return self.describe_self()
+
     def describe_content_elements(self, els, children=False):
         """
         A text description of a list of content elements, for use in
@@ -421,6 +436,8 @@
         ('attributes', 'attributes'),
         ('tag', 'tag'),
         ]
+
+    name = 'replace'
  
     def apply_transformation(self, content_type, content_els, attributes, theme_type, theme_el, log):
         describe = log.describe
@@ -537,6 +554,8 @@
 
 class Append(TransformAction):
 
+    name = 'append'
+
     # This is set to False in Prepend:
     _append = True
 
@@ -653,12 +672,15 @@
 _actions['append'] = Append
 
 class Prepend(Append):
+    name = 'prepend'
     _append = False
 
 _actions['prepend'] = Prepend
 
 class Drop(AbstractAction):
     
+    name = 'drop'
+
     def __init__(self, source_location, content, theme, if_content=None,
                  nocontent=None, notheme=None):
         self.source_location = source_location
@@ -801,3 +823,4 @@
     for p in doc.getiterator():
         if p.get(CONTENT_ATTRIB, None) is not None:
             del p.attrib[CONTENT_ATTRIB]
+

Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/ruleset.py
==============================================================================
--- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/ruleset.py	(original)
+++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/ruleset.py	Fri Jun 27 23:00:30 2008
@@ -1,8 +1,10 @@
 from deliverance.pagematch import run_matches, Match
-from deliverance.rules import Rule, remove_content_attribs
+from deliverance.rules import Rule, remove_content_attribs, AbortTheme
 from lxml.html import tostring, document_fromstring
+from lxml.etree import XML
 import re
 import urlparse
+from webob.headerdict import HeaderDict
 
 class RuleSet(object):
 
@@ -17,7 +19,10 @@
             response_headers = HeaderDict(resp.headerlist + extra_headers)
         else:
             response_headers = resp.headers
-        classes = run_matches(self.matchers, req, response_headers, log)
+        try:
+            classes = run_matches(self.matchers, req, response_headers, log)
+        except AbortTheme:
+            return resp
         if not classes:
             classes = ['default']
         rules = []
@@ -36,8 +41,14 @@
         assert theme is not None
         theme_doc = self.get_theme(theme, resource_fetcher, log)
         content_doc = self.parse_document(resp.body, req.url)
+        run_standard = True
         for rule in rules:
             rule.apply(content_doc, theme_doc, resource_fetcher, log)
+            if rule.suppress_standard:
+                run_standard = False
+        if run_standard:
+            ## FIXME: should it be possible to put the standard rule in the ruleset?
+            standard_rule.apply(content_doc, theme_doc, resource_fetcher, log)
         remove_content_attribs(theme_doc)
         ## FIXME: handle caching?
         resp.body = tostring(theme_doc)
@@ -100,3 +111,13 @@
             continue
         headers.append((http_equiv, content))
     return headers
+
+standard_rule = Rule.parse_xml(XML('''\
+<rule>
+  <!-- FIXME: append-or-replace for title? -->
+  <replace content="children:/html/head/title" theme="children:/html/head/title" nocontent="ignore" />
+  <append content="elements:/html/head/link" theme="children:/html/head" nocontent="ignore" />
+  <append content="elements:/html/head/script" theme="children:/html/head" nocontent="ignore" />
+  <append content="elements:/html/head/style" theme="children:/html/head" nocontent="ignore" />
+  <!-- FIXME: Any handling for overlapping/identical elements? -->
+</rule>'''), 'deliverance.ruleset.standard_rule')

Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/stringmatch.py
==============================================================================
--- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/stringmatch.py	(original)
+++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/stringmatch.py	Fri Jun 27 23:00:30 2008
@@ -228,10 +228,10 @@
             value = asbool(s)
         except ValueError:
             value = False
-        if not self.boolean:
-            return not value
-        else:
+        if self.boolean:
             return value
+        else:
+            return not value
 
 _add_matcher(BooleanMatcher)
 

Modified: z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/test_middleware.txt
==============================================================================
--- z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/test_middleware.txt	(original)
+++ z3/deliverance/sandbox/ianb/deliverance/trunk/deliverance/tests/test_middleware.txt	Fri Jun 27 23:00:30 2008
@@ -43,6 +43,7 @@
     ...   <match path="/blog" class="blog" />
     ...   <match path="exact:/about.html" class="breakout" />
     ...   <match request-header="X-No-Deliverate: boolean:true" abort="1" />
+    ...   <match response-header="X-No-Deliverate: boolean:true" abort="1" />
     ...   <match environ="wsgi.url_scheme: https" class="via-https" />
     ...   <theme href="/theme.html" />
     ...   <rule class="default">
@@ -61,7 +62,9 @@
 Some pages:
 
     >>> app['/blog/index.html'] = Response('''\
-    ... <html><head><title>A blog post</title></head>
+    ... <html><head><title>A blog post</title>
+    ... <link href="rss.xml" rel="alternate" type="application/rss+xml" title="RSS Feed" />
+    ... </head>
     ... <body>
     ... Some junk
     ... <div id="content">the blog post <b>with some style</b></div>
@@ -79,11 +82,17 @@
     >>> app['/magic'] = Response('''\
     ... <html><head></head><body>A simple page</body></html>''')
     >>> app['/magic'].headers['x-no-deliverate'] = '1'
+    >>> app['/magic2'] = Response('''\
+    ... <html><head><meta http-equiv="X-No-Deliverate" content="1"></head><body>something</body></html>''')
 
 Now to deliverate:
 
     >>> from deliverance.middleware import DeliveranceMiddleware, SubrequestRuleGetter
-    >>> deliv = DeliveranceMiddleware(app, SubrequestRuleGetter('/rules.xml'))
+    >>> from deliverance.log import PrintingLogger
+    >>> import logging
+    >>> deliv = DeliveranceMiddleware(app, SubrequestRuleGetter('/rules.xml'),
+    ...                               PrintingLogger,
+    ...                               log_factory_kw=dict(print_level=logging.WARNING, description=False))
 
 Now lets look at some plain content and its deliverated equivalent
 
@@ -94,9 +103,14 @@
     ...     resp = Request.blank(path).get_response(deliv)
     ...     print 'Themed content:'
     ...     print resp.body.strip()
-    >>> compare_request('/blog/index.html')
+
+First the blog, fairly simple:
+
+    >>> compare_request('/blog/index.html') # doctest: +REPORT_UDIFF
     Original content:
-    <html><head><title>A blog post</title></head>
+    <html><head><title>A blog post</title>
+    <link href="rss.xml" rel="alternate" type="application/rss+xml" title="RSS Feed" />
+    </head>
     <body>
     Some junk
     <div id="content">the blog post <b>with some style</b></div>
@@ -104,9 +118,9 @@
     <div id="footer">a footer that will be ignored</div>
     </body></html>
     Themed content:
-    <html><head><title>This is a theme title</title><link rel="Stylesheet" type="text/css" href="http://localhost/style.css"><style type="text/css">
+    <html><head><title>A blog post</title><link rel="Stylesheet" type="text/css" href="http://localhost/style.css"><style type="text/css">
         @import "http://localhost/style2.css";
-      </style></head><body>
+      </style><link href="rss.xml" rel="alternate" type="application/rss+xml" title="RSS Feed"></head><body>
     <BLANKLINE>
       <div id="header" class="title-bar">
         <h1 id="title">This is the theme title</h1>
@@ -122,6 +136,45 @@
     <BLANKLINE>
      </body></html>
 
+Now the about page, with its breakout style:
+
+    >>> compare_request('/about.html') # doctest: +REPORT_UDIFF
+    Original content:
+    <html><title>About this site</title></html>
+    <body>
+    This is all about this site.
+    <div id="footer">a footer that will be ignored</div>
+    </body></html>
+    Themed content:
+    <html><head><title>About this site</title><link rel="Stylesheet" type="text/css" href="http://localhost/style.css"><style type="text/css">
+        @import "http://localhost/style2.css";
+      </style></head><body>
+    <BLANKLINE>
+      <div id="header" class="title-bar">
+        <h1 id="title">This is the theme title</h1>
+        <div class="topnav"></div>
+      </div>
+      <div id="content-wrapper">
+    This is all about this site.
+    </div>
+    <BLANKLINE>
+      <div id="footer">a footer that will be ignored</div>
+    <BLANKLINE>
+     </body></html>
+
+Now the magic response, which shouldn't get themed at all:
+
+    >>> compare_request('/magic')
+    Original content:
+    <html><head></head><body>A simple page</body></html>
+    Themed content:
+    <html><head></head><body>A simple page</body></html>
+    >>> compare_request('/magic2')
+    Original content:
+    <html><head><meta http-equiv="X-No-Deliverate" content="1"></head><body>something</body></html>
+    Themed content:
+    <html><head><meta http-equiv="X-No-Deliverate" content="1"></head><body>something</body></html>
+
 
 Other rule formats
 ==================
@@ -206,27 +259,3 @@
       replace "children:#footer" "children:#footer" nocontent=ignore
     rule blog:
       append "children:#content" "children:#content" nocontent=abort
-
-
-
-
-    
-
-    <ruleset>
-      <match path="/blog" class="blog" />
-      <match path="exact:/about.html" class="breakout" />
-      <match request-header="X-No-Deliverate: boolean:true" abort="1" />
-      <match environ="wsgi.url_scheme: https" class="via-https" />
-      <theme href="/theme.html" />
-      <rule class="default">
-        <append content="children:body" theme="children:#content" nocontent="abort" />
-        <replace content="children:#footer" theme="children:#footer" nocontent="ignore" />
-      </rule>
-      <rule class="breakout">
-        <append content="children:body" theme="children:#content-wrapper" nocontent="abort" />
-        <replace content="children:#footer" theme="children:#footer" nocontent="ignore" />
-      </rule>
-      <rule class="blog">
-        <append content="children:#content" theme="children:#content" nocontent="abort" />
-      </rule>
-    </ruleset>


More information about the z3-checkins mailing list