Merge ~cjwatson/launchpad:inline-zope.app.wsgi into launchpad:master

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: 2928aa000d39b70ccbab796b18dc0cf63917aea4
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~cjwatson/launchpad:inline-zope.app.wsgi
Merge into: launchpad:master
Prerequisite: ~cjwatson/launchpad:inline-zope.session
Diff against target: 357 lines (+172/-30)
8 files modified
lib/lp/services/webapp/wsgi.py (+83/-1)
lib/lp/testing/layers.py (+2/-7)
lib/lp/testing/pages.py (+84/-1)
lib/lp_sitecustomize.py (+1/-12)
requirements/launchpad.txt (+0/-3)
setup.cfg (+0/-1)
utilities/list-pages (+2/-2)
zcml/zopeapp.zcml (+0/-3)
Reviewer Review Type Date Requested Status
Andrey Fedoseev (community) Approve
Review via email: mp+426610@code.launchpad.net

Commit message

Inline relevant parts of zope.app.wsgi

Description of the change

This removes a number of packages from the virtualenv:

  ZConfig
  ZODB
  zc.lockfile
  zdaemon
  zodbpickle
  zope.app.appsetup
  zope.app.wsgi
  zope.minmax
  zope.session
  zope.site

This saves perhaps 5% off startup time (measured by median of five runs of `bin/py -c ''`), 10% off the time taken to execute ZCML (measured by median of five runs of `bin/harness </dev/null`), and paves the way for some further improvements by generally trimming cruft from our dependency tree.

I considered instead pruning the tree by making `ZODB` an optional dependency of `zope.app.appsetup`, or something along those lines. However, like much of `zope.app`, `zope.app.appsetup` and `zope.app.wsgi` are fundamentally built around the assumption of running the full Zope application server. Launchpad uses the Zope Component Architecture and many Zope Toolkit components (which are intended for use in various different application server environments), but it isn't built around the Zope application server, and we've had to work around some resulting impedance mismatches for a long time. Architecturally, I think it would be better to let `zope.app` do its own thing as appropriate for that application server environment, and remove the relatively small number of remaining uses of it from Launchpad.

To post a comment you must log in.
Revision history for this message
Andrey Fedoseev (andrey-fedoseev) wrote :

Looks good

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/services/webapp/wsgi.py b/lib/lp/services/webapp/wsgi.py
2index 31d5a53..4e9dcd2 100644
3--- a/lib/lp/services/webapp/wsgi.py
4+++ b/lib/lp/services/webapp/wsgi.py
5@@ -1,5 +1,17 @@
6 # Copyright 2021 Canonical Ltd. This software is licensed under the
7 # GNU Affero General Public License version 3 (see the file LICENSE).
8+#
9+# Portions from zope.app.wsgi, which is:
10+#
11+# Copyright (c) 2004 Zope Foundation and Contributors.
12+# All Rights Reserved.
13+#
14+# This software is subject to the provisions of the Zope Public License,
15+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
16+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
17+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
19+# FOR A PARTICULAR PURPOSE.
20
21 """Main Launchpad WSGI application."""
22
23@@ -9,13 +21,15 @@ __all__ = [
24
25 import logging
26
27-from zope.app.wsgi import WSGIPublisherApplication
28+from zope.app.publication.httpfactory import HTTPPublicationRequestFactory
29 from zope.component.hooks import setHooks
30 from zope.configuration import xmlconfig
31 from zope.configuration.config import ConfigurationMachine
32 from zope.event import notify
33 from zope.interface import implementer
34 from zope.processlifetime import DatabaseOpened
35+from zope.publisher.interfaces.logginginfo import ILoggingInfo
36+from zope.publisher.publish import publish
37 from zope.security.interfaces import IParticipation
38 from zope.security.management import (
39 endInteraction,
40@@ -33,6 +47,74 @@ class SystemConfigurationParticipation:
41 interaction = None
42
43
44+# Based on zope.app.wsgi.WSGIPublisherApplication, but with fewer
45+# dependencies.
46+class WSGIPublisherApplication:
47+ """A WSGI application implementation for the Zope publisher.
48+
49+ Instances of this class can be used as a WSGI application object.
50+
51+ The class relies on a properly initialized request factory.
52+ """
53+
54+ def __init__(
55+ self, factory=HTTPPublicationRequestFactory, handle_errors=True
56+ ):
57+ self.requestFactory = None
58+ self.handleErrors = handle_errors
59+ # HTTPPublicationRequestFactory requires a "db" object, mainly for
60+ # ZODB integration. This isn't useful in Launchpad, so just pass a
61+ # meaningless object.
62+ self.requestFactory = factory(object())
63+
64+ def __call__(self, environ, start_response):
65+ """Called by a WSGI server.
66+
67+ The ``environ`` parameter is a dictionary object, containing CGI-style
68+ environment variables. This object must be a builtin Python dictionary
69+ (not a subclass, UserDict or other dictionary emulation), and the
70+ application is allowed to modify the dictionary in any way it
71+ desires. The dictionary must also include certain WSGI-required
72+ variables (described in a later section), and may also include
73+ server-specific extension variables, named according to a convention
74+ that will be described below.
75+
76+ The ``start_response`` parameter is a callable accepting two required
77+ positional arguments, and one optional argument. For the sake of
78+ illustration, we have named these arguments ``status``,
79+ ``response_headers``, and ``exc_info``, but they are not required to
80+ have these names, and the application must invoke the
81+ ``start_response`` callable using positional arguments
82+ (e.g. ``start_response(status, response_headers)``).
83+ """
84+ request = self.requestFactory(environ["wsgi.input"], environ)
85+
86+ # Let's support post-mortem debugging
87+ handle_errors = environ.get("wsgi.handleErrors", self.handleErrors)
88+
89+ request = publish(request, handle_errors=handle_errors)
90+ response = request.response
91+ # Get logging info from principal for log use
92+ logging_info = ILoggingInfo(request.principal, None)
93+ if logging_info is None:
94+ message = b"-"
95+ else:
96+ message = logging_info.getLogMessage()
97+
98+ # Convert message bytes to native string
99+ message = message.decode("latin1")
100+
101+ environ["wsgi.logging_info"] = message
102+ if "REMOTE_USER" not in environ:
103+ environ["REMOTE_USER"] = message
104+
105+ # Start the WSGI server response
106+ start_response(response.getStatusString(), response.getHeaders())
107+
108+ # Return the result body iterable.
109+ return response.consumeBodyIter()
110+
111+
112 def get_wsgi_application():
113 # Loosely based on zope.app.appsetup.appsetup.
114 features = []
115diff --git a/lib/lp/testing/layers.py b/lib/lp/testing/layers.py
116index 4539ae2..1a98734 100644
117--- a/lib/lp/testing/layers.py
118+++ b/lib/lp/testing/layers.py
119@@ -77,7 +77,6 @@ from storm.uri import URI
120 from talisker.context import Context
121 from webob.request import environ_from_url as orig_environ_from_url
122 from wsgi_intercept import httplib2_intercept
123-from zope.app.wsgi import WSGIPublisherApplication
124 from zope.component import getUtility, globalregistry, provideUtility
125 from zope.component.testlayer import ZCMLFileLayer
126 from zope.event import notify
127@@ -118,6 +117,7 @@ from lp.services.webapp.interfaces import IOpenLaunchBag
128 from lp.services.webapp.servers import (
129 register_launchpad_request_publication_factories,
130 )
131+from lp.services.webapp.wsgi import WSGIPublisherApplication
132 from lp.testing import ANONYMOUS, login, logout, reset_logging
133 from lp.testing.pgsql import PgTestSetup
134
135@@ -1039,11 +1039,6 @@ class _FunctionalBrowserLayer(zope.testbrowser.wsgi.Layer, ZCMLFileLayer):
136 from the one zope.testrunner expects.
137 """
138
139- # A meaningless object passed to publication classes that just require
140- # something other than None. In Zope this would be a ZODB connection,
141- # but we don't use ZODB in Launchpad.
142- fake_db = object()
143-
144 def __init__(self, *args, **kwargs):
145 super().__init__(*args, **kwargs)
146 self.middlewares = [
147@@ -1088,7 +1083,7 @@ class _FunctionalBrowserLayer(zope.testbrowser.wsgi.Layer, ZCMLFileLayer):
148
149 def make_wsgi_app(self):
150 """See `zope.testbrowser.wsgi.Layer`."""
151- return self.setupMiddleware(WSGIPublisherApplication(self.fake_db))
152+ return self.setupMiddleware(WSGIPublisherApplication())
153
154
155 class FunctionalLayer(BaseLayer):
156diff --git a/lib/lp/testing/pages.py b/lib/lp/testing/pages.py
157index f0eb960..accac95 100644
158--- a/lib/lp/testing/pages.py
159+++ b/lib/lp/testing/pages.py
160@@ -1,5 +1,17 @@
161 # Copyright 2009-2019 Canonical Ltd. This software is licensed under the
162 # GNU Affero General Public License version 3 (see the file LICENSE).
163+#
164+# Portions from zope.app.wsgi.testlayer, which is:
165+#
166+# Copyright (c) 2010 Zope Foundation and Contributors.
167+# All Rights Reserved.
168+#
169+# This software is subject to the provisions of the Zope Public License,
170+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
171+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
172+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
173+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
174+# FOR A PARTICULAR PURPOSE.
175
176 """Testing infrastructure for page tests."""
177
178@@ -29,7 +41,6 @@ from lazr.restful.testing.webservice import WebServiceCaller
179 from oauthlib import oauth1
180 from soupsieve import escape as css_escape
181 from webtest import TestRequest
182-from zope.app.wsgi.testlayer import FakeResponse, NotInBrowserLayer
183 from zope.component import getUtility
184 from zope.security.management import setSecurityPolicy
185 from zope.security.proxy import removeSecurityProxy
186@@ -76,6 +87,78 @@ SAMPLEDATA_ACCESS_SECRETS = {
187 }
188
189
190+class NotInBrowserLayer(Exception):
191+ """The current test is not running in zope.testbrowser.wsgi.Layer."""
192+
193+
194+# Based on zope.app.wsgi.testlayer.FakeResponse, but with fewer dependencies.
195+class FakeResponse:
196+ """This behaves like a Response object returned by HTTPCaller of
197+ zope.app.testing.functional.
198+ """
199+
200+ def __init__(self, response, request=None):
201+ self.response = response
202+ self.request = request
203+
204+ @property
205+ def server_protocol(self):
206+ protocol = None
207+ if self.request is not None:
208+ protocol = self.request.environ.get("SERVER_PROTOCOL")
209+ if protocol is None:
210+ protocol = b"HTTP/1.0"
211+ if not isinstance(protocol, bytes):
212+ protocol = protocol.encode("latin1")
213+ return protocol
214+
215+ def getStatus(self):
216+ return self.response.status_int
217+
218+ def getStatusString(self):
219+ return self.response.status
220+
221+ def getHeader(self, name, default=None):
222+ return self.response.headers.get(name, default)
223+
224+ def getHeaders(self):
225+ return sorted(self.response.headerlist)
226+
227+ def getBody(self):
228+ return self.response.body
229+
230+ def getOutput(self):
231+ status = self.response.status
232+ status = (
233+ status.encode("latin1")
234+ if not isinstance(status, bytes)
235+ else status
236+ )
237+ parts = [self.server_protocol + b" " + status]
238+
239+ headers = [
240+ (
241+ k.encode("latin1") if not isinstance(k, bytes) else k,
242+ v.encode("latin1") if not isinstance(v, bytes) else v,
243+ )
244+ for k, v in self.getHeaders()
245+ ]
246+
247+ parts += [k + b": " + v for k, v in headers]
248+
249+ body = self.response.body
250+ if body:
251+ if not isinstance(body, bytes):
252+ body = body.encode("utf-8")
253+ parts += [b"", body]
254+ return b"\n".join(parts)
255+
256+ __bytes__ = getOutput
257+
258+ def __str__(self):
259+ return self.getOutput().decode("latin-1")
260+
261+
262 def http(string, handle_errors=True):
263 """Make a test HTTP request.
264
265diff --git a/lib/lp_sitecustomize.py b/lib/lp_sitecustomize.py
266index 16d71fa..4624d29 100644
267--- a/lib/lp_sitecustomize.py
268+++ b/lib/lp_sitecustomize.py
269@@ -24,18 +24,7 @@ from lp.services.openid.fetcher import set_default_openid_fetcher
270
271
272 def add_custom_loglevels():
273- """Add out custom log levels to the Python logging package."""
274-
275- # This import installs custom ZODB loglevels, which we can then
276- # override. BLATHER is between INFO and DEBUG, so we can leave it.
277- # TRACE conflicts with DEBUG6, and since we are not using ZEO, we
278- # just overwrite the level string by calling addLevelName.
279- from ZODB.loglevels import BLATHER, TRACE
280-
281- # Confirm our above assumptions, and silence lint at the same time.
282- assert BLATHER == 15
283- assert TRACE == loglevels.DEBUG6
284-
285+ """Add our custom log levels to the Python logging package."""
286 logging.addLevelName(loglevels.DEBUG2, "DEBUG2")
287 logging.addLevelName(loglevels.DEBUG3, "DEBUG3")
288 logging.addLevelName(loglevels.DEBUG4, "DEBUG4")
289diff --git a/requirements/launchpad.txt b/requirements/launchpad.txt
290index 38b4e60..b4b4af3 100644
291--- a/requirements/launchpad.txt
292+++ b/requirements/launchpad.txt
293@@ -192,10 +192,7 @@ zipp==1.2.0
294 zope.app.http==4.0.1
295 zope.app.publication==4.3.1
296 zope.app.publisher==4.2.0
297-zope.app.wsgi==4.3.0
298 zope.publisher==6.0.2+lp1
299-# lp:~launchpad-committers/zope.session:launchpad
300-zope.session==4.3.0+lp1
301 zope.testbrowser==5.5.1
302 # lp:~launchpad-committers/zope.testrunner:launchpad
303 zope.testrunner==5.3.0+lp1
304diff --git a/setup.cfg b/setup.cfg
305index c9d841e..70311e8 100644
306--- a/setup.cfg
307+++ b/setup.cfg
308@@ -121,7 +121,6 @@ install_requires =
309 zope.app.http
310 zope.app.publication
311 zope.app.publisher
312- zope.app.wsgi[testlayer]
313 zope.authentication
314 zope.browser
315 zope.browsermenu
316diff --git a/utilities/list-pages b/utilities/list-pages
317index 5c720cf..0e64904 100755
318--- a/utilities/list-pages
319+++ b/utilities/list-pages
320@@ -47,7 +47,6 @@ import _pythonpath # noqa: F401
321 import os
322 from inspect import getmro
323
324-from zope.app.wsgi.testlayer import BrowserLayer
325 from zope.browserpage.simpleviewclass import simple
326 from zope.component import adapter, getGlobalSiteManager
327 from zope.interface import directlyProvides, implementer
328@@ -58,6 +57,7 @@ from lp.services.config import config
329 from lp.services.scripts import execute_zcml_for_scripts
330 from lp.services.webapp.interfaces import ICanonicalUrlData
331 from lp.services.webapp.publisher import canonical_url
332+from lp.testing.layers import _FunctionalBrowserLayer
333
334 ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
335
336@@ -69,7 +69,7 @@ def load_zcml(zopeless=False):
337 if zopeless:
338 execute_zcml_for_scripts()
339 else:
340- BrowserLayer(zcml, zcml_file="webapp.zcml").setUp()
341+ _FunctionalBrowserLayer(zcml, zcml_file="webapp.zcml").setUp()
342
343
344 def is_page_adapter(a):
345diff --git a/zcml/zopeapp.zcml b/zcml/zopeapp.zcml
346index 9ae82dd..16cc755 100644
347--- a/zcml/zopeapp.zcml
348+++ b/zcml/zopeapp.zcml
349@@ -81,9 +81,6 @@
350 provides="zope.i18n.interfaces.IModifiableUserPreferredLanguages"
351 />
352
353-
354- <include package="zope.app.wsgi" />
355-
356 <!-- Default XMLRPC pre-marshalling. -->
357 <include package="zope.publisher" />
358

Subscribers

People subscribed via source and target branches

to status/vote changes: