Merge ~cjwatson/launchpad:scripts-utilities-js-future-imports into launchpad:master

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: eb5bd07abacda491dac633d21173854485bda9a8
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~cjwatson/launchpad:scripts-utilities-js-future-imports
Merge into: launchpad:master
Diff against target: 555 lines (+94/-170)
10 files modified
lib/lp/scripts/utilities/js/combinecss.py (+2/-0)
lib/lp/scripts/utilities/js/combo.py (+6/-31)
lib/lp/scripts/utilities/js/jsbuild.py (+26/-28)
lib/lp/scripts/utilities/js/tests/test_combo.py (+3/-61)
lib/lp/services/encoding.py (+19/-1)
lib/lp/services/tests/test_encoding.py (+33/-3)
lib/lp/services/webapp/servers.py (+1/-15)
lib/lp/services/webapp/tests/test_servers.py (+1/-26)
lib/lp/testing/layers.py (+1/-1)
lib/lp/testing/pages.py (+2/-4)
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+377136@code.launchpad.net

Commit message

Convert lp.scripts.utilities.js to preferred __future__ imports

Also remove the unused combo_app.

Description of the change

This requires care because of WSGI's requirement for native strings in some places.

To post a comment you must log in.
eb5bd07... by Colin Watson

Remove lp.scripts.utilities.js.combo.combo_app

It was unused; we use convoy for this instead.

Revision history for this message
Colin Watson (cjwatson) wrote :

At William's suggestion, I removed combo_app entirely, since it's unused. We might as well keep the rest of this port, though; I think the various type changes are still correct, and wsgi_native_string's new home is somewhat more appropriate.

Revision history for this message
William Grant (wgrant) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/scripts/utilities/js/combinecss.py b/lib/lp/scripts/utilities/js/combinecss.py
2index 2047d25..1aa1072 100755
3--- a/lib/lp/scripts/utilities/js/combinecss.py
4+++ b/lib/lp/scripts/utilities/js/combinecss.py
5@@ -1,6 +1,8 @@
6 # Copyright 2009-2018 Canonical Ltd. This software is licensed under the
7 # GNU Affero General Public License version 3 (see the file LICENSE).
8
9+from __future__ import absolute_import, print_function, unicode_literals
10+
11 import os
12
13 import scandir
14diff --git a/lib/lp/scripts/utilities/js/combo.py b/lib/lp/scripts/utilities/js/combo.py
15index 4d27293..f568bc2 100644
16--- a/lib/lp/scripts/utilities/js/combo.py
17+++ b/lib/lp/scripts/utilities/js/combo.py
18@@ -1,6 +1,8 @@
19 # Copyright 2011-2019 Canonical Ltd. This software is licensed under the
20 # GNU Affero General Public License version 3 (see the file LICENSE).
21
22+from __future__ import absolute_import, print_function, unicode_literals
23+
24 __metaclass__ = type
25
26 import os
27@@ -8,7 +10,7 @@ import urlparse
28
29 from six.moves.urllib.parse import parse_qsl
30
31-from jsbuild import (
32+from lp.scripts.utilities.js.jsbuild import (
33 CSSComboFile,
34 JSComboFile,
35 )
36@@ -32,7 +34,7 @@ def parse_qs(query):
37 return tuple([param for param, value in params])
38
39
40-def combine_files(fnames, root, resource_prefix="",
41+def combine_files(fnames, root, resource_prefix=b"",
42 minify_css=True, rewrite_urls=True):
43 """Combine many files into one.
44
45@@ -56,33 +58,6 @@ def combine_files(fnames, root, resource_prefix="",
46 if not full.startswith(root) or not os.path.exists(full):
47 yield combo.get_comment("[missing]")
48 else:
49- f = open(full, "r")
50- content = f.read()
51- f.close()
52+ with open(full, "rb") as f:
53+ content = f.read()
54 yield combo.filter_file_content(content, full)
55-
56-
57-def combo_app(root, resource_prefix="", minify_css=True, rewrite_urls=True):
58- """A simple YUI Combo Service WSGI app.
59-
60- Serves any files under C{root}, setting an appropriate
61- C{Content-Type} header.
62- """
63- root = os.path.abspath(root)
64-
65- def app(environ, start_response, root=root):
66- fnames = parse_qs(environ["QUERY_STRING"])
67- content_type = "text/plain"
68- if fnames:
69- if fnames[0].endswith(".js"):
70- content_type = "text/javascript"
71- elif fnames[0].endswith(".css"):
72- content_type = "text/css"
73- else:
74- start_response("404 Not Found", [("Content-Type", content_type)])
75- return ("Not Found",)
76- start_response("200 OK", [("Content-Type", content_type)])
77- return combine_files(fnames, root, resource_prefix,
78- minify_css, rewrite_urls)
79-
80- return app
81diff --git a/lib/lp/scripts/utilities/js/jsbuild.py b/lib/lp/scripts/utilities/js/jsbuild.py
82index 5499ea7..14841c0 100644
83--- a/lib/lp/scripts/utilities/js/jsbuild.py
84+++ b/lib/lp/scripts/utilities/js/jsbuild.py
85@@ -3,6 +3,8 @@
86
87 """build.py - Minifies and creates the JS build directory."""
88
89+from __future__ import absolute_import, print_function, unicode_literals
90+
91 __metaclass__ = type
92 __all__ = [
93 'CSSComboFile',
94@@ -26,9 +28,9 @@ BUILD_DIR = os.path.normpath(os.path.join(
95 DEFAULT_SRC_DIR = os.path.normpath(os.path.join(
96 HERE, os.pardir, os.pardir, os.pardir, 'app', 'javascript'))
97
98-ESCAPE_STAR_PROPERTY_RE = re.compile(r'\*([a-zA-Z0-9_-]+):')
99-UNESCAPE_STAR_PROPERTY_RE = re.compile(r'([a-zA-Z0-9_-]+)_ie_star_hack:')
100-URL_RE = re.compile("url\([ \"\']*([^ \"\']+)[ \"\']*\)")
101+ESCAPE_STAR_PROPERTY_RE = re.compile(br'\*([a-zA-Z0-9_-]+):')
102+UNESCAPE_STAR_PROPERTY_RE = re.compile(br'([a-zA-Z0-9_-]+)_ie_star_hack:')
103+URL_RE = re.compile(br"url\([ \"\']*([^ \"\']+)[ \"\']*\)")
104
105
106 def relative_path(from_file, to_file):
107@@ -77,37 +79,33 @@ class ComboFile:
108
109 def update(self):
110 """Update the file from its source files."""
111- target_fh = open(self.target_file, 'w')
112- try:
113+ with open(self.target_file, 'wb') as target_fh:
114 for src_file in self.src_files:
115 self.log("Processing '%s'" % os.path.basename(src_file))
116 target_fh.write(self.get_file_header(src_file))
117- fh = open(src_file)
118- content = fh.read()
119- fh.close()
120+ with open(src_file, 'rb') as fh:
121+ content = fh.read()
122 try:
123 target_fh.write(
124 self.filter_file_content(content, src_file))
125 except Exception:
126 os.remove(self.target_file)
127 raise
128- finally:
129- target_fh.close()
130
131 def get_comment(self, msg):
132- """Return a string wrapped in a comment to be include in the output.
133+ """Return a byte string wrapped in a comment to include in the output.
134
135 Can be used to help annotate the output file.
136 """
137- return ''
138+ return b''
139
140 def get_file_header(self, path):
141- """Return a string to include before outputting a file.
142+ """Return a byte string to include before outputting a file.
143
144 Can be used by subclasses to output a file reference in the combined
145 file. Default implementation returns nothing.
146 """
147- return ''
148+ return b''
149
150 def filter_file_content(self, file_content, path):
151 """Hook to process the file content before being combined."""
152@@ -122,13 +120,13 @@ class JSComboFile(ComboFile):
153 """
154
155 def get_comment(self, msg):
156- return "// %s\n" % msg
157+ return b"// %s\n" % msg.encode("UTF-8")
158
159 def get_file_header(self, path):
160 return self.get_comment(relative_path(self.target_file, path))
161
162 def filter_file_content(self, file_content, path):
163- return file_content + '\n'
164+ return file_content + b'\n'
165
166
167 class CSSComboFile(ComboFile):
168@@ -138,15 +136,15 @@ class CSSComboFile(ComboFile):
169 to the new location, and minify the result.
170 """
171
172- def __init__(self, src_files, target_file, resource_prefix="",
173+ def __init__(self, src_files, target_file, resource_prefix=b"",
174 minify=True, rewrite_urls=True):
175 super(CSSComboFile, self).__init__(src_files, target_file)
176- self.resource_prefix = resource_prefix.rstrip("/")
177+ self.resource_prefix = resource_prefix.rstrip(b"/")
178 self.minify = minify
179 self.rewrite_urls = rewrite_urls
180
181 def get_comment(self, msg):
182- return "/* %s */\n" % msg
183+ return b"/* %s */\n" % msg.encode("UTF-8")
184
185 def get_file_header(self, path):
186 return self.get_comment(relative_path(self.target_file, path))
187@@ -164,18 +162,18 @@ class CSSComboFile(ComboFile):
188 def fix_relative_url(match):
189 url = match.group(1)
190 # Don't modify absolute URLs or 'data:' urls.
191- if (url.startswith("http") or
192- url.startswith("/") or
193- url.startswith("data:")):
194+ if (url.startswith(b"http") or
195+ url.startswith(b"/") or
196+ url.startswith(b"data:")):
197 return match.group(0)
198 parts = relative_parts + url.split("/")
199 result = []
200 for part in parts:
201- if part == ".." and result and result[-1] != "..":
202+ if part == b".." and result and result[-1] != b"..":
203 result.pop(-1)
204 continue
205 result.append(part)
206- return "url(%s)" % "/".join(
207+ return b"url(%s)" % b"/".join(
208 filter(None, [self.resource_prefix] + result))
209 file_content = URL_RE.sub(fix_relative_url, file_content)
210
211@@ -186,16 +184,16 @@ class CSSComboFile(ComboFile):
212 cssutils.ser.prefs.useMinified()
213
214 stylesheet = ESCAPE_STAR_PROPERTY_RE.sub(
215- r'\1_ie_star_hack:', file_content)
216+ br'\1_ie_star_hack:', file_content)
217 settings.set('DXImageTransform.Microsoft', True)
218 parser = cssutils.CSSParser(raiseExceptions=True)
219 css = parser.parseString(stylesheet)
220 stylesheet = UNESCAPE_STAR_PROPERTY_RE.sub(
221- r'*\1:', css.cssText)
222- return stylesheet + "\n"
223+ br'*\1:', css.cssText)
224+ return stylesheet + b"\n"
225 finally:
226 cssutils.setSerializer(old_serializer)
227- return file_content + "\n"
228+ return file_content + b"\n"
229
230
231 class Builder:
232diff --git a/lib/lp/scripts/utilities/js/tests/test_combo.py b/lib/lp/scripts/utilities/js/tests/test_combo.py
233index b1c8bb7..c745a6c 100644
234--- a/lib/lp/scripts/utilities/js/tests/test_combo.py
235+++ b/lib/lp/scripts/utilities/js/tests/test_combo.py
236@@ -1,17 +1,16 @@
237 # Copyright 2011-2012 Canonical Ltd. This software is licensed under the
238 # GNU Affero General Public License version 3 (see the file LICENSE).
239
240+from __future__ import absolute_import, print_function, unicode_literals
241+
242 __metaclass__ = type
243
244 import os
245 import shutil
246 import tempfile
247
248-from paste.fixture import TestApp
249-
250 from lp.scripts.utilities.js.combo import (
251 combine_files,
252- combo_app,
253 parse_url,
254 )
255 from lp.testing import TestCase
256@@ -435,7 +434,7 @@ class TestCombo(ComboTestBase):
257 "".join(combine_files(["widget/assets/skins/sam/widget.css",
258 "editor/assets/skins/sam/editor.css"],
259 root=test_dir,
260- resource_prefix="/static/")).strip(),
261+ resource_prefix=b"/static/")).strip(),
262 expected)
263
264 def test_missing_file_is_ignored(self):
265@@ -513,60 +512,3 @@ class TestCombo(ComboTestBase):
266 self.assertEqual(
267 "".join(combine_files([hack], root=test_dir)).strip(),
268 expected)
269-
270-
271-class TestWSGICombo(ComboTestBase):
272-
273- def setUp(self):
274- super(TestWSGICombo, self).setUp()
275- self.root = self.makeDir()
276- self.app = TestApp(combo_app(self.root))
277-
278- def test_combo_app_sets_content_type_for_js(self):
279- """The WSGI App should set a proper Content-Type for Javascript."""
280- self.makeSampleFile(
281- self.root,
282- os.path.join("yui", "yui-min.js"),
283- "** yui-min **")
284- self.makeSampleFile(
285- self.root,
286- os.path.join("oop", "oop-min.js"),
287- "** oop-min **")
288- self.makeSampleFile(
289- self.root,
290- os.path.join("event-custom", "event-custom-min.js"),
291- "** event-custom-min **")
292-
293- expected = "\n".join(("// yui/yui-min.js",
294- "** yui-min **",
295- "// oop/oop-min.js",
296- "** oop-min **",
297- "// event-custom/event-custom-min.js",
298- "** event-custom-min **"))
299-
300- res = self.app.get("/?" + "&".join(
301- ["yui/yui-min.js",
302- "oop/oop-min.js",
303- "event-custom/event-custom-min.js"]), status=200)
304- self.assertEqual(res.headers, [("Content-Type", "text/javascript")])
305- self.assertEqual(res.body.strip(), expected)
306-
307- def test_combo_app_sets_content_type_for_css(self):
308- """The WSGI App should set a proper Content-Type for CSS."""
309- self.makeSampleFile(
310- self.root,
311- os.path.join("widget", "skin", "sam", "widget.css"),
312- "/* widget-skin-sam */")
313-
314- expected = "/* widget/skin/sam/widget.css */"
315-
316- res = self.app.get("/?" + "&".join(
317- ["widget/skin/sam/widget.css"]), status=200)
318- self.assertEqual(res.headers, [("Content-Type", "text/css")])
319- self.assertEqual(res.body.strip(), expected)
320-
321- def test_no_filename_gives_404(self):
322- """If no filename is included, a 404 should be returned."""
323- res = self.app.get("/", status=404)
324- self.assertEqual(res.headers, [("Content-Type", "text/plain")])
325- self.assertEqual(res.body, "Not Found")
326diff --git a/lib/lp/services/encoding.py b/lib/lp/services/encoding.py
327index 2483176..7feb308 100644
328--- a/lib/lp/services/encoding.py
329+++ b/lib/lp/services/encoding.py
330@@ -1,4 +1,4 @@
331-# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
332+# Copyright 2009-2019 Canonical Ltd. This software is licensed under the
333 # GNU Affero General Public License version 3 (see the file LICENSE).
334
335 """Character encoding utilities"""
336@@ -8,11 +8,14 @@ __all__ = [
337 'escape_nonascii_uniquely',
338 'guess',
339 'is_ascii_only',
340+ 'wsgi_native_string',
341 ]
342
343 import codecs
344 import re
345
346+import six
347+
348
349 _boms = [
350 (codecs.BOM_UTF16_BE, 'utf_16_be'),
351@@ -212,3 +215,18 @@ def is_ascii_only(string):
352 return False
353 else:
354 return True
355+
356+
357+def wsgi_native_string(s):
358+ """Make a native string suitable for use in WSGI.
359+
360+ PEP 3333 requires environment variables to be native strings that
361+ contain only code points representable in ISO-8859-1. To support
362+ porting to Python 3 via an intermediate stage of Unicode literals in
363+ Python 2, we enforce this here.
364+ """
365+ result = six.ensure_str(s, encoding='ISO-8859-1')
366+ if six.PY3 and isinstance(s, six.text_type):
367+ # Ensure we're limited to ISO-8859-1.
368+ result.encode('ISO-8859-1')
369+ return result
370diff --git a/lib/lp/services/tests/test_encoding.py b/lib/lp/services/tests/test_encoding.py
371index 1d91552..4a0e235 100644
372--- a/lib/lp/services/tests/test_encoding.py
373+++ b/lib/lp/services/tests/test_encoding.py
374@@ -1,14 +1,44 @@
375-# Copyright 2009 Canonical Ltd. This software is licensed under the
376+# Copyright 2009-2019 Canonical Ltd. This software is licensed under the
377 # GNU Affero General Public License version 3 (see the file LICENSE).
378
379 from doctest import (
380 DocTestSuite,
381 ELLIPSIS,
382 )
383+import unittest
384+
385+import six
386
387 import lp.services.encoding
388+from lp.services.encoding import wsgi_native_string
389+from lp.testing import TestCase
390+
391+
392+class TestWSGINativeString(TestCase):
393+
394+ def _toNative(self, s):
395+ if six.PY3:
396+ return s
397+ else:
398+ return s.encode('ISO-8859-1')
399+
400+ def test_not_bytes_or_unicode(self):
401+ self.assertRaises(TypeError, wsgi_native_string, object())
402+
403+ def test_bytes_iso_8859_1(self):
404+ self.assertEqual(
405+ self._toNative(u'foo\xfe'), wsgi_native_string(b'foo\xfe'))
406+
407+ def test_unicode_iso_8859_1(self):
408+ self.assertEqual(
409+ self._toNative(u'foo\xfe'), wsgi_native_string(u'foo\xfe'))
410+
411+ def test_unicode_not_iso_8859_1(self):
412+ self.assertRaises(UnicodeEncodeError, wsgi_native_string, u'foo\u2014')
413
414
415 def test_suite():
416- suite = DocTestSuite(lp.services.encoding, optionflags=ELLIPSIS)
417- return suite
418+ return unittest.TestSuite((
419+ unittest.TestLoader().loadTestsFromName(__name__),
420+ DocTestSuite(lp.services.encoding, optionflags=ELLIPSIS),
421+ ))
422diff --git a/lib/lp/services/webapp/servers.py b/lib/lp/services/webapp/servers.py
423index c9fc0e6..e99fb05 100644
424--- a/lib/lp/services/webapp/servers.py
425+++ b/lib/lp/services/webapp/servers.py
426@@ -64,6 +64,7 @@ from lp.app import versioninfo
427 from lp.app.errors import UnexpectedFormData
428 import lp.layers
429 from lp.services.config import config
430+from lp.services.encoding import wsgi_native_string
431 from lp.services.features import get_relevant_feature_controller
432 from lp.services.features.flags import NullFeatureController
433 from lp.services.feeds.interfaces.application import IFeedsApplication
434@@ -534,21 +535,6 @@ def get_query_string_params(request):
435 return decoded_qs
436
437
438-def wsgi_native_string(s):
439- """Make a native string suitable for use in WSGI.
440-
441- PEP 3333 requires environment variables to be native strings that
442- contain only code points representable in ISO-8859-1. To support
443- porting to Python 3 via an intermediate stage of Unicode literals in
444- Python 2, we enforce this here.
445- """
446- result = six.ensure_str(s, encoding='ISO-8859-1')
447- if six.PY3 and isinstance(s, six.text_type):
448- # Ensure we're limited to ISO-8859-1.
449- result.encode('ISO-8859-1')
450- return result
451-
452-
453 class LaunchpadBrowserRequestMixin:
454 """Provides methods used for both API and web browser requests."""
455
456diff --git a/lib/lp/services/webapp/tests/test_servers.py b/lib/lp/services/webapp/tests/test_servers.py
457index 79595e6..02afdb7 100644
458--- a/lib/lp/services/webapp/tests/test_servers.py
459+++ b/lib/lp/services/webapp/tests/test_servers.py
460@@ -1,4 +1,4 @@
461-# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
462+# Copyright 2009-2019 Canonical Ltd. This software is licensed under the
463 # GNU Affero General Public License version 3 (see the file LICENSE).
464
465 __metaclass__ = type
466@@ -21,7 +21,6 @@ from lazr.restful.testing.webservice import (
467 IGenericEntry,
468 WebServiceTestCase,
469 )
470-import six
471 from zope.component import (
472 getGlobalSiteManager,
473 getUtility,
474@@ -48,7 +47,6 @@ from lp.services.webapp.servers import (
475 WebServicePublication,
476 WebServiceRequestPublicationFactory,
477 WebServiceTestRequest,
478- wsgi_native_string,
479 )
480 from lp.testing import (
481 EventRecorder,
482@@ -355,29 +353,6 @@ class TestWebServiceRequest(WebServiceTestCase):
483 request.response.getHeader('Vary'), 'Accept')
484
485
486-class TestWSGINativeString(TestCase):
487-
488- def _toNative(self, s):
489- if six.PY3:
490- return s
491- else:
492- return s.encode('ISO-8859-1')
493-
494- def test_not_bytes_or_unicode(self):
495- self.assertRaises(TypeError, wsgi_native_string, object())
496-
497- def test_bytes_iso_8859_1(self):
498- self.assertEqual(
499- self._toNative(u'foo\xfe'), wsgi_native_string(b'foo\xfe'))
500-
501- def test_unicode_iso_8859_1(self):
502- self.assertEqual(
503- self._toNative(u'foo\xfe'), wsgi_native_string(u'foo\xfe'))
504-
505- def test_unicode_not_iso_8859_1(self):
506- self.assertRaises(UnicodeEncodeError, wsgi_native_string, u'foo\u2014')
507-
508-
509 class TestBasicLaunchpadRequest(TestCase):
510 """Tests for the base request class"""
511
512diff --git a/lib/lp/testing/layers.py b/lib/lp/testing/layers.py
513index 59617f5..65fb63e 100644
514--- a/lib/lp/testing/layers.py
515+++ b/lib/lp/testing/layers.py
516@@ -121,6 +121,7 @@ from lp.services.config.fixture import (
517 )
518 from lp.services.database.interfaces import IStore
519 from lp.services.database.sqlbase import session_store
520+from lp.services.encoding import wsgi_native_string
521 from lp.services.job.tests import celery_worker
522 from lp.services.librarian.model import LibraryFileAlias
523 from lp.services.librarianserver.testing.server import LibrarianServerFixture
524@@ -147,7 +148,6 @@ from lp.services.webapp.interfaces import IOpenLaunchBag
525 from lp.services.webapp.servers import (
526 LaunchpadAccessLogger,
527 register_launchpad_request_publication_factories,
528- wsgi_native_string,
529 )
530 import lp.services.webapp.session
531 from lp.testing import (
532diff --git a/lib/lp/testing/pages.py b/lib/lp/testing/pages.py
533index 79bfba5..1c23980 100644
534--- a/lib/lp/testing/pages.py
535+++ b/lib/lp/testing/pages.py
536@@ -79,6 +79,7 @@ from lp.services.beautifulsoup import (
537 SoupStrainer,
538 )
539 from lp.services.config import config
540+from lp.services.encoding import wsgi_native_string
541 from lp.services.oauth.interfaces import (
542 IOAuthConsumerSet,
543 OAUTH_REALM,
544@@ -86,10 +87,7 @@ from lp.services.oauth.interfaces import (
545 from lp.services.webapp import canonical_url
546 from lp.services.webapp.authorization import LaunchpadPermissiveSecurityPolicy
547 from lp.services.webapp.interfaces import OAuthPermission
548-from lp.services.webapp.servers import (
549- LaunchpadTestRequest,
550- wsgi_native_string,
551- )
552+from lp.services.webapp.servers import LaunchpadTestRequest
553 from lp.services.webapp.url import urlsplit
554 from lp.testing import (
555 ANONYMOUS,

Subscribers

People subscribed via source and target branches

to status/vote changes: