D2792: hgweb: port archive command to modern response API

indygreg (Gregory Szorc) phabricator at mercurial-scm.org
Mon Mar 12 17:34:12 EDT 2018


This revision was automatically updated to reflect the committed changes.
Closed by commit rHG97f44b0720e2: hgweb: port archive command to modern response API (authored by indygreg, committed by ).

REPOSITORY
  rHG Mercurial

CHANGES SINCE LAST UPDATE
  https://phab.mercurial-scm.org/D2792?vs=6853&id=6928

REVISION DETAIL
  https://phab.mercurial-scm.org/D2792

AFFECTED FILES
  hgext/keyword.py
  mercurial/hgweb/hgweb_mod.py
  mercurial/hgweb/request.py
  mercurial/hgweb/webcommands.py
  tests/hgweberror.py

CHANGE DETAILS

diff --git a/tests/hgweberror.py b/tests/hgweberror.py
--- a/tests/hgweberror.py
+++ b/tests/hgweberror.py
@@ -10,9 +10,12 @@
     '''Dummy web command that raises an uncaught Exception.'''
 
     # Simulate an error after partial response.
-    if 'partialresponse' in req.req.qsparams:
-        req.respond(200, 'text/plain')
-        req.write('partial content\n')
+    if 'partialresponse' in web.req.qsparams:
+        web.res.status = b'200 Script output follows'
+        web.res.headers[b'Content-Type'] = b'text/plain'
+        web.res.setbodywillwrite()
+        list(web.res.sendresponse())
+        web.res.getbodyfile().write(b'partial content\n')
 
     raise AttributeError('I am an uncaught error!')
 
diff --git a/mercurial/hgweb/webcommands.py b/mercurial/hgweb/webcommands.py
--- a/mercurial/hgweb/webcommands.py
+++ b/mercurial/hgweb/webcommands.py
@@ -19,14 +19,10 @@
     ErrorResponse,
     HTTP_FORBIDDEN,
     HTTP_NOT_FOUND,
-    HTTP_OK,
     get_contact,
     paritygen,
     staticfile,
 )
-from . import (
-    request as requestmod,
-)
 
 from .. import (
     archival,
@@ -64,7 +60,9 @@
     The function can return the ``requestcontext.res`` instance to signal
     that it wants to use this object to generate the response. If an iterable
     is returned, the ``wsgirequest`` instance will be used and the returned
-    content will constitute the response body.
+    content will constitute the response body. ``True`` can be returned to
+    indicate that the function already sent output and the caller doesn't
+    need to do anything more to send the response.
 
     Usage:
 
@@ -1210,21 +1208,24 @@
                     'file(s) not found: %s' % file)
 
     mimetype, artype, extension, encoding = web.archivespecs[type_]
-    headers = [
-        ('Content-Disposition', 'attachment; filename=%s%s' % (name, extension))
-        ]
+
+    web.res.headers['Content-Type'] = mimetype
+    web.res.headers['Content-Disposition'] = 'attachment; filename=%s%s' % (
+        name, extension)
+
     if encoding:
-        headers.append(('Content-Encoding', encoding))
-    req.headers.extend(headers)
-    req.respond(HTTP_OK, mimetype)
+        web.res.headers['Content-Encoding'] = encoding
 
-    bodyfh = requestmod.offsettrackingwriter(req.write)
+    web.res.setbodywillwrite()
+    assert list(web.res.sendresponse()) == []
+
+    bodyfh = web.res.getbodyfile()
 
     archival.archive(web.repo, bodyfh, cnode, artype, prefix=name,
                      matchfn=match,
                      subrepos=web.configbool("web", "archivesubrepos"))
-    return []
 
+    return True
 
 @webcommand('static')
 def static(web, req, tmpl):
diff --git a/mercurial/hgweb/request.py b/mercurial/hgweb/request.py
--- a/mercurial/hgweb/request.py
+++ b/mercurial/hgweb/request.py
@@ -351,22 +351,42 @@
 
         self._bodybytes = None
         self._bodygen = None
+        self._bodywillwrite = False
         self._started = False
+        self._bodywritefn = None
+
+    def _verifybody(self):
+        if (self._bodybytes is not None or self._bodygen is not None
+            or self._bodywillwrite):
+            raise error.ProgrammingError('cannot define body multiple times')
 
     def setbodybytes(self, b):
         """Define the response body as static bytes."""
-        if self._bodybytes is not None or self._bodygen is not None:
-            raise error.ProgrammingError('cannot define body multiple times')
-
+        self._verifybody()
         self._bodybytes = b
         self.headers['Content-Length'] = '%d' % len(b)
 
     def setbodygen(self, gen):
         """Define the response body as a generator of bytes."""
-        if self._bodybytes is not None or self._bodygen is not None:
-            raise error.ProgrammingError('cannot define body multiple times')
+        self._verifybody()
+        self._bodygen = gen
+
+    def setbodywillwrite(self):
+        """Signal an intent to use write() to emit the response body.
+
+        **This is the least preferred way to send a body.**
 
-        self._bodygen = gen
+        It is preferred for WSGI applications to emit a generator of chunks
+        constituting the response body. However, some consumers can't emit
+        data this way. So, WSGI provides a way to obtain a ``write(data)``
+        function that can be used to synchronously perform an unbuffered
+        write.
+
+        Calling this function signals an intent to produce the body in this
+        manner.
+        """
+        self._verifybody()
+        self._bodywillwrite = True
 
     def sendresponse(self):
         """Send the generated response to the client.
@@ -384,7 +404,8 @@
         if not self.status:
             raise error.ProgrammingError('status line not defined')
 
-        if self._bodybytes is None and self._bodygen is None:
+        if (self._bodybytes is None and self._bodygen is None
+            and not self._bodywillwrite):
             raise error.ProgrammingError('response body not defined')
 
         # Various HTTP clients (notably httplib) won't read the HTTP response
@@ -434,15 +455,40 @@
                 if not chunk:
                     break
 
-        self._startresponse(pycompat.sysstr(self.status), self.headers.items())
+        write = self._startresponse(pycompat.sysstr(self.status),
+                                    self.headers.items())
+
         if self._bodybytes:
             yield self._bodybytes
         elif self._bodygen:
             for chunk in self._bodygen:
                 yield chunk
+        elif self._bodywillwrite:
+            self._bodywritefn = write
         else:
             error.ProgrammingError('do not know how to send body')
 
+    def getbodyfile(self):
+        """Obtain a file object like object representing the response body.
+
+        For this to work, you must call ``setbodywillwrite()`` and then
+        ``sendresponse()`` first. ``sendresponse()`` is a generator and the
+        function won't run to completion unless the generator is advanced. The
+        generator yields not items. The easiest way to consume it is with
+        ``list(res.sendresponse())``, which should resolve to an empty list -
+        ``[]``.
+        """
+        if not self._bodywillwrite:
+            raise error.ProgrammingError('must call setbodywillwrite() first')
+
+        if not self._started:
+            raise error.ProgrammingError('must call sendresponse() first; did '
+                                         'you remember to consume it since it '
+                                         'is a generator?')
+
+        assert self._bodywritefn
+        return offsettrackingwriter(self._bodywritefn)
+
 class wsgirequest(object):
     """Higher-level API for a WSGI request.
 
diff --git a/mercurial/hgweb/hgweb_mod.py b/mercurial/hgweb/hgweb_mod.py
--- a/mercurial/hgweb/hgweb_mod.py
+++ b/mercurial/hgweb/hgweb_mod.py
@@ -408,10 +408,11 @@
 
                 if content is res:
                     return res.sendresponse()
-
-                wsgireq.respond(HTTP_OK, ctype)
-
-            return content
+                elif content is True:
+                    return []
+                else:
+                    wsgireq.respond(HTTP_OK, ctype)
+                    return content
 
         except (error.LookupError, error.RepoLookupError) as err:
             wsgireq.respond(HTTP_NOT_FOUND, ctype)
diff --git a/hgext/keyword.py b/hgext/keyword.py
--- a/hgext/keyword.py
+++ b/hgext/keyword.py
@@ -624,6 +624,9 @@
         res = orig(web, req, tmpl)
         if res is web.res:
             res = res.sendresponse()
+        elif res is True:
+            return
+
         for chunk in res:
             yield chunk
     finally:



To: indygreg, #hg-reviewers, durin42
Cc: mercurial-devel


More information about the Mercurial-devel mailing list