[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