Commits

Robert Brewer committed e155283

New tools.accept(media). See test_misc_tools.py for usage.

  • Participants
  • Parent commits 70fc66e

Comments (0)

Files changed (3)

File cherrypy/_cptools.py

 _d.digest_auth = Tool('on_start_resource', auth.digest_auth)
 _d.trailing_slash = Tool('before_handler', cptools.trailing_slash)
 _d.flatten = Tool('before_finalize', cptools.flatten)
+_d.accept = Tool('on_start_resource', cptools.accept)
 
 del _d, cptools, encoding, auth, static, tidy

File cherrypy/lib/cptools.py

     response = cherrypy.response
     response.body = flattener(response.body)
 
+
+def accept(media=None):
+    """Return the client's preferred media-type (from the given Content-Types).
+    
+    If 'media' is None (the default), no test will be performed.
+    
+    If 'media' is provided, it should be the Content-Type value (as a string)
+    or values (as a list or tuple of strings) which the current request
+    can emit. The client's acceptable media ranges (as declared in the
+    Accept request header) will be matched in order to these Content-Type
+    values; the first such string is returned. That is, the return value
+    will always be one of the strings provided in the 'media' arg (or None
+    if 'media' is None).
+    
+    If no match is found, then HTTPError 406 (Not Acceptable) is raised.
+    Note that most web browsers send */* as a (low-quality) acceptable
+    media range, which should match any Content-Type. In addition, "...if
+    no Accept header field is present, then it is assumed that the client
+    accepts all media types."
+    
+    Matching types are checked in order of client preference first,
+    and then in the order of the given 'media' values.
+    
+    Note that this function does not honor accept-params (other than "q").
+    """
+    if not media:
+        return
+    if isinstance(media, basestring):
+        media = [media]
+    
+    # Parse the Accept request header, and try to match one
+    # of the requested media-ranges (in order of preference).
+    ranges = cherrypy.request.headers.elements('Accept')
+    if not ranges:
+        # Any media type is acceptable.
+        return media[0]
+    else:
+        # Note that 'ranges' is sorted in order of preference
+        for element in ranges:
+            if element.qvalue > 0:
+                if element.value == "*/*":
+                    # Matches any type or subtype
+                    return media[0]
+                elif element.value.endswith("/*"):
+                    # Matches any subtype
+                    mtype = element.value[:-1]  # Keep the slash
+                    for m in media:
+                        if m.startswith(mtype):
+                            return m
+                else:
+                    # Matches exact value
+                    if element.value in media:
+                        return element.value
+    
+    # No suitable media-range found.
+    ah = cherrypy.request.headers.get('Accept')
+    if ah is None:
+        msg = "Your client did not send an Accept header."
+    else:
+        msg = "Your client sent this Accept header: %s." % ah
+    msg += (" But this resource only emits these media types: %s." %
+            ", ".join(media))
+    raise cherrypy.HTTPError(406, msg)
+

File cherrypy/test/test_misc_tools.py

                                                ('Content-Type', 'text/plain')],
             }
     
+    
+    class Accept:
+        _cp_config = {'tools.accept.on': True}
+        
+        def index(self):
+            return '<a href="feed">Atom feed</a>'
+        index.exposed = True
+        
+        # In Python 2.4+, we could use a decorator instead:
+        # @tools.accept('application/atom+xml')
+        def feed(self):
+            return """<?xml version="1.0" encoding="utf-8"?>
+<feed xmlns="http://www.w3.org/2005/Atom">
+    <title>Unknown Blog</title>
+</feed>"""
+        feed.exposed = True
+        feed._cp_config = {'tools.accept.media': 'application/atom+xml'}
+        
+        def select(self):
+            # We could also write this: mtype = cherrypy.lib.accept.accept(...)
+            mtype = tools.accept.callable(['text/html', 'text/plain'])
+            if mtype == 'text/html':
+                return "<h2>Page Title</h2>"
+            else:
+                return "PAGE TITLE"
+        select.exposed = True
+    
     class Referer:
         def accept(self):
             return "Accepted!"
     
     root = Root()
     root.referer = Referer()
+    root.accept = Accept()
     cherrypy.tree.mount(root, config=conf)
     cherrypy.config.update({'environment': 'test_suite'})
 
         self.assertErrorPage(403, 'Forbidden Referer header.')
 
 
+class AcceptTest(helper.CPWebCase):
+    
+    def test_Accept_Tool(self):
+        # Test with no header provided
+        self.getPage('/accept/feed')
+        self.assertStatus(200)
+        self.assertInBody('<title>Unknown Blog</title>')
+        
+        # Specify exact media type
+        self.getPage('/accept/feed', headers=[('Accept', 'application/atom+xml')])
+        self.assertStatus(200)
+        self.assertInBody('<title>Unknown Blog</title>')
+        
+        # Specify matching media range
+        self.getPage('/accept/feed', headers=[('Accept', 'application/*')])
+        self.assertStatus(200)
+        self.assertInBody('<title>Unknown Blog</title>')
+        
+        # Specify all media ranges
+        self.getPage('/accept/feed', headers=[('Accept', '*/*')])
+        self.assertStatus(200)
+        self.assertInBody('<title>Unknown Blog</title>')
+        
+        # Specify unacceptable media types
+        self.getPage('/accept/feed', headers=[('Accept', 'text/html')])
+        self.assertErrorPage(406,
+                             "Your client sent this Accept header: text/html. "
+                             "But this resource only emits these media types: "
+                             "application/atom+xml.")
+        
+        # Test resource where tool is 'on' but media is None (not set).
+        self.getPage('/accept/')
+        self.assertStatus(200)
+        self.assertBody('<a href="feed">Atom feed</a>')
+    
+    def test_accept_selection(self):
+        # Try both our expected media types
+        self.getPage('/accept/select', [('Accept', 'text/html')])
+        self.assertStatus(200)
+        self.assertBody('<h2>Page Title</h2>')
+        self.getPage('/accept/select', [('Accept', 'text/plain')])
+        self.assertStatus(200)
+        self.assertBody('PAGE TITLE')
+        self.getPage('/accept/select', [('Accept', 'text/plain, text/*;q=0.5')])
+        self.assertStatus(200)
+        self.assertBody('PAGE TITLE')
+        
+        # text/* and */* should prefer text/html since it comes first
+        # in our 'media' argument to tools.accept
+        self.getPage('/accept/select', [('Accept', 'text/*')])
+        self.assertStatus(200)
+        self.assertBody('<h2>Page Title</h2>')
+        self.getPage('/accept/select', [('Accept', '*/*')])
+        self.assertStatus(200)
+        self.assertBody('<h2>Page Title</h2>')
+        
+        # Try unacceptable media types
+        self.getPage('/accept/select', [('Accept', 'application/xml')])
+        self.assertErrorPage(406,
+                             "Your client sent this Accept header: application/xml. "
+                             "But this resource only emits these media types: "
+                             "text/html, text/plain.")
+
+
+
 if __name__ == "__main__":
     setup_server()
     helper.testmain()