Merge ~bryce/ubuntu/+source/convoy:convoy-fix-x-focal into ubuntu/+source/convoy:debian/sid
- Git
- lp:~bryce/ubuntu/+source/convoy
- convoy-fix-x-focal
- Merge into debian/sid
Status: | Rejected |
---|---|
Rejected by: | Bryce Harrington |
Proposed branch: | ~bryce/ubuntu/+source/convoy:convoy-fix-x-focal |
Merge into: | ubuntu/+source/convoy:debian/sid |
Diff against target: |
1469 lines (+553/-297) 15 files modified
Makefile (+30/-10) convoy/combo.py (+87/-32) convoy/meta.py (+24/-25) convoy/tests/__init__.py (+76/-0) convoy/tests/test_combo.py (+165/-101) convoy/tests/test_meta.py (+101/-79) debian/changelog (+22/-0) debian/control (+14/-11) debian/python-convoy.install (+1/-0) debian/python3-convoy.install (+1/-0) debian/rules (+13/-2) debian/tests/control (+1/-1) debian/tests/testsuite (+1/-1) setup.py (+12/-35) tox.ini (+5/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Christian Ehrhardt (community) | Needs Fixing | ||
Canonical Server Core Reviewers | Pending | ||
Review via email: mp+377346@code.launchpad.net |
Commit message
Description of the change
Drops py2 support for convoy, in order to resolve proposed migration block.
python3-django-maas still depends on the py3 binary package, but nothing depends on the py2 binary package.
The autopkgtest for this package ran only against python2 and required python-mocker, which has no python3 equivalent. However, running the tests manually I found it worked fine with python3 and didn't complain about python-mocker.
The package's compat level is old (as are a few other aspects of the package, like use of bzr, etc.) due to that it's not been actively maintained in quite some time. I opted to keep changes to a minimum though, and only changed what was required to get it building without py2. If you think more extensive updating is warranted let me know.
A PPA with the package is here:
https:/
Bryce Harrington (bryce) wrote : | # |
Christian Ehrhardt (paelzer) wrote : | # |
For the diff I think the merge target would need to be focal-devel instead of debian/sid.
But it can be reviewed as-is.
Christian Ehrhardt (paelzer) wrote : | # |
Ack that there is no reverse depends on python-convoy
Ack to the general changes
Nit Picks:
- changelog doesn't mention e.g. d/rules changes for --buildsystem=
- maybe split the long paragraph in sub-bullets prefixed by the files the chagne takes place
All of the above is style and you could even say "nah I like mine better".
+1 to the actual functional changes.
Christian Ehrhardt (paelzer) wrote : | # |
I ran the updated packages .dsc through autopkgtest against the PPA
The result seem to need some test/polishing
$ sudo ~/work/
[...]
autopkgtest [07:46:57]: test testsuite: [------
+ python3 -m unittest discover convoy/tests
E......
=======
ERROR: test_combo (unittest.
-------
ImportError: Failed to import test module: test_combo
Traceback (most recent call last):
File "/usr/lib/
module = self._get_
File "/usr/lib/
__import_
File "/tmp/autopkgte
from webtest import TestApp
ModuleNotFoundE
-------
Ran 22 tests in 0.032s
FAILED (errors=1)
I'd not want to upload as-is - did you run the tests on your side and they were ok?
Bryce Harrington (bryce) wrote : | # |
doko beat us to the upload:
https:/
Unmerged commits
- 0e08aeb... by Bryce Harrington
-
* Drop python2 package in favor of python3.
- Change autopkgtest to use python3 dependencies. Drop dependence on
python-mocker since there's no py3 version of it; the testsuite
doesn't appear to need it when run under python3. - 6fbd26b... by Bryce Harrington
-
changelog
- b614438... by Matthias Klose
-
Import patches-unapplied version 0.2.1+bzr39-1build1 to ubuntu/
focal-proposed Imported using git-ubuntu import.
Changelog parent: da3a596fb204f35
ce1fe367b1ebdaf 6ce4e81a23 New changelog entries:
* No-change rebuild to generate dependencies on python2. - da3a596... by Andres Rodriguez
-
Import patches-unapplied version 0.2.1+bzr39-1 to ubuntu/
xenial- proposed Imported using git-ubuntu import.
Changelog parent: b9b26230def71a3
b0ad1c83a2a3c62 4cbec0edc2 New changelog entries:
* New upstream snapshot.
* Build with python3.
Preview Diff
1 | diff --git a/Makefile b/Makefile |
2 | index e9a4e00..b322f5b 100644 |
3 | --- a/Makefile |
4 | +++ b/Makefile |
5 | @@ -1,13 +1,33 @@ |
6 | -PY=$(shell which python) |
7 | +PYTHON := python |
8 | |
9 | -.PHONY: test |
10 | -test: |
11 | - $(PY) setup.py test |
12 | +# --- |
13 | |
14 | -.PHONY: build |
15 | -build: |
16 | - $(PY) setup.py sdist |
17 | +dist: bin/python setup.py README |
18 | + bin/python setup.py egg_info sdist |
19 | |
20 | -.PHONY: upload |
21 | -upload: |
22 | - $(PY) setup.py sdist upload |
23 | +upload: bin/python setup.py README |
24 | + bin/python setup.py egg_info sdist upload |
25 | + |
26 | +test: bin/tox |
27 | + @bin/tox |
28 | + |
29 | +clean: |
30 | + $(RM) -r bin build dist include lib local TAGS tags |
31 | + find . -name '*.py[co]' -print0 | xargs -r0 $(RM) |
32 | + find . -name '__pycache__' -print0 | xargs -r0 $(RM) -r |
33 | + find . -name '*.egg' -print0 | xargs -r0 $(RM) -r |
34 | + find . -name '*.egg-info' -print0 | xargs -r0 $(RM) -r |
35 | + find . -name '*~' -print0 | xargs -r0 $(RM) |
36 | + $(RM) -r .eggs .tox _trial_temp |
37 | + |
38 | +# --- |
39 | + |
40 | +bin/tox: bin/pip |
41 | + bin/pip install --quiet --ignore-installed tox |
42 | + |
43 | +bin/python bin/pip: |
44 | + virtualenv --python=$(PYTHON) --quiet $(CURDIR) |
45 | + |
46 | +# --- |
47 | + |
48 | +.PHONY: dist upload test clean |
49 | diff --git a/convoy/combo.py b/convoy/combo.py |
50 | index da27dd3..664f4d9 100644 |
51 | --- a/convoy/combo.py |
52 | +++ b/convoy/combo.py |
53 | @@ -1,5 +1,5 @@ |
54 | # Convoy is a WSGI app for loading multiple files in the same request. |
55 | -# Copyright (C) 2010-2012 Canonical, Ltd. |
56 | +# Copyright (C) 2011-2015 Canonical, Ltd. |
57 | # |
58 | # This program is free software: you can redistribute it and/or modify |
59 | # it under the terms of the GNU Affero General Public License as |
60 | @@ -15,15 +15,25 @@ |
61 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
62 | |
63 | |
64 | -import cgi |
65 | -import os |
66 | +import logging |
67 | +import os.path |
68 | import re |
69 | -import urlparse |
70 | +import sys |
71 | + |
72 | +try: |
73 | + from urllib.parse import parse_qsl |
74 | +except ImportError: |
75 | + from cgi import parse_qsl |
76 | + |
77 | +try: |
78 | + import urllib.parse as urlparse |
79 | +except ImportError: |
80 | + import urlparse |
81 | |
82 | |
83 | CHUNK_SIZE = 2 << 12 |
84 | -URL_RE = re.compile("url\([ \"\']*([^ \"\']+)[ \"\']*\)") |
85 | -URL_PARSE = re.compile("/([^/]*).*?$") |
86 | +URL_RE = re.compile(b"""url[(][ "']*([^ "')]+)[ "']*[)]""") |
87 | +URL_PARSE = re.compile(b"/([^/]*).*?$") |
88 | |
89 | |
90 | def relative_path(from_file, to_file): |
91 | @@ -50,10 +60,27 @@ def parse_qs(query): |
92 | |
93 | Returns the list of arguments in the original order. |
94 | """ |
95 | - params = cgi.parse_qsl(query, keep_blank_values=True) |
96 | + params = parse_qsl(query, keep_blank_values=True) |
97 | return tuple([param for param, value in params]) |
98 | |
99 | |
100 | +class InvalidFileError(Exception): |
101 | + """Exception raised for bogus filenames.""" |
102 | + |
103 | + |
104 | +def validate_files(fnames, root): |
105 | + """Validate that the given filenames are sane. |
106 | + |
107 | + Filenames must be within the root directory and actual files. |
108 | + |
109 | + @raises InvalidFileError for any bogus files. |
110 | + """ |
111 | + for fname in fnames: |
112 | + full = os.path.abspath(os.path.join(root, fname)) |
113 | + if not (full.startswith(root) and os.path.isfile(full)): |
114 | + raise InvalidFileError(fname) |
115 | + |
116 | + |
117 | def combine_files(fnames, root, resource_prefix="", rewrite_urls=True): |
118 | """Combine many files into one. |
119 | |
120 | @@ -61,59 +88,77 @@ def combine_files(fnames, root, resource_prefix="", rewrite_urls=True): |
121 | files. The relative path to root will be included as a comment |
122 | between each file. |
123 | """ |
124 | + # Used when encoding local file-system paths. |
125 | + fsenc = sys.getfilesystemencoding() |
126 | + |
127 | + if not isinstance(root, bytes): |
128 | + # This is a local file-system path. |
129 | + root = root.encode(fsenc) |
130 | + if not isinstance(resource_prefix, bytes): |
131 | + # This is a URL component, so assume only ASCII is okay. |
132 | + resource_prefix = resource_prefix.encode("ascii") |
133 | + |
134 | + resource_prefix = resource_prefix.rstrip(b"/") |
135 | |
136 | - resource_prefix = resource_prefix.rstrip("/") |
137 | for fname in fnames: |
138 | + if not isinstance(fname, bytes): |
139 | + # This is a local file-system path. |
140 | + fname = fname.encode(fsenc) |
141 | + |
142 | file_ext = os.path.splitext(fname)[-1] |
143 | basename = os.path.basename(fname) |
144 | full = os.path.abspath(os.path.join(root, fname)) |
145 | - yield "/* " + fname + " */\n" |
146 | - if not full.startswith(root) or not os.path.exists(full): |
147 | - yield "/* [missing] */\n" |
148 | - else: |
149 | - with open(full, "r") as f: |
150 | - if file_ext == ".css" and rewrite_urls: |
151 | + if (full.startswith(root) and os.path.isfile(full)): |
152 | + yield b"/* " + fname + b" */\n" |
153 | + with open(full, "rb") as f: |
154 | + if file_ext == b".css" and rewrite_urls: |
155 | file_content = f.read() |
156 | src_dir = os.path.dirname(full) |
157 | relative_parts = relative_path( |
158 | os.path.join(root, basename), src_dir).split( |
159 | - os.path.sep) |
160 | + os.path.sep.encode(fsenc)) |
161 | |
162 | def fix_relative_url(match): |
163 | url = match.group(1) |
164 | # Don't modify absolute URLs or 'data:' urls. |
165 | - if (url.startswith("http") or |
166 | - url.startswith("/") or |
167 | - url.startswith("data:")): |
168 | + if (url.startswith(b"http") or |
169 | + url.startswith(b"/") or |
170 | + url.startswith(b"data:")): |
171 | return match.group(0) |
172 | - parts = relative_parts + url.split("/") |
173 | + parts = relative_parts + url.split(b"/") |
174 | result = [] |
175 | for part in parts: |
176 | - if part == ".." and result and result[-1] != "..": |
177 | + if part == b".." and result[-1:] != [b".."]: |
178 | result.pop(-1) |
179 | continue |
180 | result.append(part) |
181 | - return "url(%s)" % "/".join( |
182 | - filter(None, [resource_prefix] + result)) |
183 | + return b"url(x)".replace(b"x", b"/".join( |
184 | + part for part in [resource_prefix] + result |
185 | + if part)) |
186 | + |
187 | file_content = URL_RE.sub(fix_relative_url, file_content) |
188 | yield file_content |
189 | - yield "\n" |
190 | + yield b"\n" |
191 | else: |
192 | while True: |
193 | chunk = f.read(CHUNK_SIZE) |
194 | if not chunk: |
195 | - yield "\n" |
196 | + yield b"\n" |
197 | break |
198 | yield chunk |
199 | |
200 | |
201 | -def combo_app(root, resource_prefix="", rewrite_urls=True): |
202 | +def combo_app( |
203 | + root, resource_prefix="", rewrite_urls=True, additional_headers=None): |
204 | """A simple YUI Combo Service WSGI app. |
205 | |
206 | Serves any files under C{root}, setting an appropriate |
207 | C{Content-Type} header. |
208 | + Additional headers can be provided as a list of tuples to allow |
209 | + for generic extensions, but their correctness won't be verified. |
210 | """ |
211 | root = os.path.abspath(root) |
212 | + log = logging.getLogger(__name__) |
213 | |
214 | def app(environ, start_response, root=root): |
215 | # Path hint uses the rest of the url to map to files on disk based off |
216 | @@ -127,8 +172,9 @@ def combo_app(root, resource_prefix="", rewrite_urls=True): |
217 | elif fnames[0].endswith(".css"): |
218 | content_type = "text/css" |
219 | else: |
220 | + log.info('No files in querystring.') |
221 | start_response("404 Not Found", [("Content-Type", content_type)]) |
222 | - return ("Not Found",) |
223 | + return (b"Not Found",) |
224 | |
225 | # Take any prefix in the url route into consideration for the root to |
226 | # find files. |
227 | @@ -136,12 +182,21 @@ def combo_app(root, resource_prefix="", rewrite_urls=True): |
228 | # Enforce that the updated root is not outside the original root. |
229 | absroot = os.path.abspath(updated_root) |
230 | if not absroot.startswith(os.path.abspath(root)): |
231 | + log.info('Updated root is outside of original root.') |
232 | start_response("400 Bad Request", [("Content-Type", content_type)]) |
233 | - return ("Bad Request",) |
234 | + return (b"Bad Request",) |
235 | + headers = [("Content-Type", content_type), |
236 | + ("X-Content-Type-Options", "nosniff")] |
237 | + if additional_headers is not None: |
238 | + headers.extend(additional_headers) |
239 | + try: |
240 | + validate_files(fnames, updated_root) |
241 | + except InvalidFileError as if_error: |
242 | + log.info('No such file: %s' % if_error.args[0]) |
243 | + start_response("400 Bad Request", headers) |
244 | + return (b"Bad Request",) |
245 | else: |
246 | - start_response("200 OK", [("Content-Type", content_type), |
247 | - ("X-Content-Type-Options", "nosniff")]) |
248 | - |
249 | - return combine_files(fnames, updated_root, resource_prefix, |
250 | - rewrite_urls=rewrite_urls) |
251 | + start_response("200 OK", headers) |
252 | + return combine_files(fnames, updated_root, resource_prefix, |
253 | + rewrite_urls=rewrite_urls) |
254 | return app |
255 | diff --git a/convoy/meta.py b/convoy/meta.py |
256 | index ed9f3ed..d38c2c7 100644 |
257 | --- a/convoy/meta.py |
258 | +++ b/convoy/meta.py |
259 | @@ -1,5 +1,5 @@ |
260 | # Convoy is a WSGI app for loading multiple files in the same request. |
261 | -# Copyright (C) 2010-2012 Canonical, Ltd. |
262 | +# Copyright (C) 2011-2015 Canonical, Ltd. |
263 | # |
264 | # This program is free software: you can redistribute it and/or modify |
265 | # it under the terms of the GNU Affero General Public License as |
266 | @@ -35,6 +35,7 @@ DETAILS_REPLACE = r'"\1":' |
267 | |
268 | LITERAL_RE = re.compile("([\[ ]+)\"([\w\.\+-]+)\"([^:])") |
269 | NAME_RE = re.compile("[\.\+-]") |
270 | +COMMENT_RE = re.compile("\/\/.*") |
271 | |
272 | |
273 | def extract_metadata(src): |
274 | @@ -44,6 +45,7 @@ def extract_metadata(src): |
275 | name, details, ignore = entry.groups() |
276 | details = details.replace('\'', '"') |
277 | details = DETAILS_FIND.sub(DETAILS_REPLACE, details) |
278 | + details = COMMENT_RE.sub('', details) |
279 | details = json.loads(details) |
280 | details["name"] = name |
281 | metadata.append(details) |
282 | @@ -100,7 +102,8 @@ class Builder: |
283 | |
284 | for fname in fnames: |
285 | self.log("Extracting metadata from '%s'" % fname) |
286 | - data = open(fname, "r").read() |
287 | + with open(fname, "r") as fd: |
288 | + data = fd.read() |
289 | meta = extract_metadata(data) |
290 | prefix = "" |
291 | if self.prefix and not prefix.endswith("/"): |
292 | @@ -167,13 +170,12 @@ class Builder: |
293 | return match.group(1) + literals_map[literal] + match.group(3) |
294 | return match.group(0) |
295 | |
296 | - |
297 | linebreak = ",\n " |
298 | variables_decl = "var SKIN_SAM_PREFIX = 'skin-sam-'" + linebreak |
299 | if self.prefix: |
300 | - variables_decl += "PREFIX = '%s'%s" % (self.prefix, linebreak) |
301 | + variables_decl += "PREFIX = '%s'%s" % (self.prefix, linebreak) |
302 | extra_variables = [] |
303 | - for literal, variable in sorted(literals_map.iteritems()): |
304 | + for literal, variable in sorted(literals_map.items()): |
305 | extra_variable = "%s = %s" % ( |
306 | variable, ('"%s"' % literal).replace( |
307 | '"skin-sam-', 'SKIN_SAM_PREFIX + "')) |
308 | @@ -202,11 +204,11 @@ class Builder: |
309 | "after_list"]) |
310 | |
311 | modules_decl = [] |
312 | - for module_name, module_info in sorted(modules.iteritems()): |
313 | + for module_name, module_info in sorted(modules.items()): |
314 | module_decl = [ |
315 | "modules[%s] = module_info = {}" % |
316 | NAME_RE.sub("_", module_name).upper()] |
317 | - for key, value in sorted(module_info.iteritems()): |
318 | + for key, value in sorted(module_info.items()): |
319 | if value is True or value is False: |
320 | value = str(value).upper() |
321 | elif value in ("css", "js"): |
322 | @@ -217,8 +219,8 @@ class Builder: |
323 | # It's easy to think that doing 'CORE_CSS + %(values)s' |
324 | # instead of using concat would work, but it doesn't; |
325 | # you'll end up with a string instead of a list. |
326 | - module_decl.append("after_list = CORE_CSS"); |
327 | - module_decl.append("after_list.concat(%s)" % value); |
328 | + module_decl.append("after_list = CORE_CSS") |
329 | + module_decl.append("after_list.concat(%s)" % value) |
330 | value = "after_list" |
331 | if key == "path": |
332 | value = value.replace( |
333 | @@ -229,8 +231,7 @@ class Builder: |
334 | |
335 | modules_decl = ";\n\n ".join(modules_decl) |
336 | |
337 | - module_config = open(out, "w") |
338 | - try: |
339 | + with open(out, "w") as module_config: |
340 | module_config.write("""var %s = (function(){ |
341 | %s; |
342 | |
343 | @@ -238,8 +239,6 @@ class Builder: |
344 | |
345 | return modules; |
346 | })();""" % (var_name, variables_decl, modules_decl)) |
347 | - finally: |
348 | - module_config.close() |
349 | |
350 | def generate_skin_modules(self, entry, metadata, root): |
351 | # Generate a skin module definition, since YUI assumes that |
352 | @@ -369,15 +368,15 @@ def get_options(): |
353 | |
354 | |
355 | def main(): |
356 | - options, args = get_options() |
357 | - if options.src_dir is None: |
358 | - options.src_dir = os.getcwd() |
359 | - Builder( |
360 | - name=options.name, |
361 | - src_dir=os.path.abspath(options.src_dir), |
362 | - output=options.output, |
363 | - prefix=options.prefix, |
364 | - exclude_regex=options.exclude_regex, |
365 | - ext=options.ext, |
366 | - include_skin=not options.no_skin, |
367 | - ).do_build() |
368 | + options, args = get_options() |
369 | + if options.src_dir is None: |
370 | + options.src_dir = os.getcwd() |
371 | + Builder( |
372 | + name=options.name, |
373 | + src_dir=os.path.abspath(options.src_dir), |
374 | + output=options.output, |
375 | + prefix=options.prefix, |
376 | + exclude_regex=options.exclude_regex, |
377 | + ext=options.ext, |
378 | + include_skin=not options.no_skin, |
379 | + ).do_build() |
380 | diff --git a/convoy/tests/__init__.py b/convoy/tests/__init__.py |
381 | index e69de29..aef8cae 100644 |
382 | --- a/convoy/tests/__init__.py |
383 | +++ b/convoy/tests/__init__.py |
384 | @@ -0,0 +1,76 @@ |
385 | +# Convoy is a WSGI app for loading multiple files in the same request. |
386 | +# Copyright (C) 2011-2015 Canonical, Ltd. |
387 | +# |
388 | +# This program is free software: you can redistribute it and/or modify |
389 | +# it under the terms of the GNU Affero General Public License as |
390 | +# published by the Free Software Foundation, either version 3 of the |
391 | +# License, or (at your option) any later version. |
392 | +# |
393 | +# This program is distributed in the hope that it will be useful, |
394 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
395 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
396 | +# GNU Affero General Public License for more details. |
397 | +# |
398 | +# You should have received a copy of the GNU Affero General Public License |
399 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
400 | + |
401 | +import os |
402 | +import os.path |
403 | +import shutil |
404 | +import tempfile |
405 | +import unittest |
406 | + |
407 | +__all__ = [ |
408 | + "ConvoyTestCase", |
409 | +] |
410 | + |
411 | + |
412 | +class ConvoyTestCase(unittest.TestCase): |
413 | + |
414 | + def makeDir(self, path=None): |
415 | + """Create a temporary directory. |
416 | + |
417 | + This directory will be removed when the test completes. |
418 | + |
419 | + :param path: An optional directory path to create. If not specified a |
420 | + new directory path will be chosen by `tempfile.mkdtemp`. |
421 | + :return: The path of the newly created directory. |
422 | + """ |
423 | + if path is None: |
424 | + path = tempfile.mkdtemp() |
425 | + self.addCleanup(shutil.rmtree, path) |
426 | + return path |
427 | + else: |
428 | + os.makedirs(path) |
429 | + return path |
430 | + |
431 | + def makeFile(self, content=None, basename=None, dirname=None, path=None): |
432 | + """Create a temporary file. |
433 | + |
434 | + This file will be removed when the test completes. |
435 | + |
436 | + :param content: Optional contents for the new file. |
437 | + :param basename: Optional base name for the new file. |
438 | + :param dirname: Optional directory in which to put the new file. |
439 | + :param path: Optional full path to the new file. Mutually exclusive |
440 | + with `basename` and `dirname`. |
441 | + :return: The path of the newly created file. |
442 | + """ |
443 | + if path is not None: |
444 | + self.assertIsNone(basename) |
445 | + self.assertIsNone(dirname) |
446 | + self.addCleanup(os.unlink, path) |
447 | + elif basename is not None: |
448 | + if dirname is None: |
449 | + dirname = self.makeDir() |
450 | + path = os.path.join(dirname, basename) |
451 | + else: |
452 | + fileno, path = tempfile.mkstemp(dir=dirname) |
453 | + self.addCleanup(os.unlink, path) |
454 | + os.close(fileno) |
455 | + |
456 | + if content is not None: |
457 | + with open(path, "w") as fd: |
458 | + fd.write(content) |
459 | + |
460 | + return path |
461 | diff --git a/convoy/tests/test_combo.py b/convoy/tests/test_combo.py |
462 | index bb9d60d..c291510 100644 |
463 | --- a/convoy/tests/test_combo.py |
464 | +++ b/convoy/tests/test_combo.py |
465 | @@ -1,5 +1,5 @@ |
466 | # Convoy is a WSGI app for loading multiple files in the same request. |
467 | -# Copyright (C) 2010-2012 Canonical, Ltd. |
468 | +# Copyright (C) 2011-2015 Canonical, Ltd. |
469 | # |
470 | # This program is free software: you can redistribute it and/or modify |
471 | # it under the terms of the GNU Affero General Public License as |
472 | @@ -18,17 +18,18 @@ |
473 | import os |
474 | import difflib |
475 | import textwrap |
476 | -from unittest import defaultTestLoader |
477 | |
478 | -import mocker |
479 | -from paste.fixture import TestApp |
480 | +from webtest import TestApp |
481 | |
482 | from convoy.combo import combo_app |
483 | from convoy.combo import combine_files |
484 | from convoy.combo import parse_url |
485 | +from convoy.combo import validate_files |
486 | +from convoy.combo import InvalidFileError |
487 | +from convoy.tests import ConvoyTestCase |
488 | |
489 | |
490 | -class ComboTestBase(object): |
491 | +class ComboTestBase(ConvoyTestCase): |
492 | |
493 | def makeSampleFile(self, root, fname, content): |
494 | content = textwrap.dedent(content).strip() |
495 | @@ -38,19 +39,23 @@ class ComboTestBase(object): |
496 | os.makedirs(parent) |
497 | return self.makeFile(content=content, path=full) |
498 | |
499 | - def assertTextEquals(self, expected, got): |
500 | + def assertTextEquals(self, expected, observed): |
501 | + if isinstance(expected, bytes): |
502 | + expected = expected.decode("utf8") |
503 | + if isinstance(observed, bytes): |
504 | + observed = observed.decode("utf8") |
505 | expected = textwrap.dedent(expected).strip() |
506 | - got = textwrap.dedent(got).strip() |
507 | + observed = textwrap.dedent(observed).strip() |
508 | diff = difflib.unified_diff(expected.splitlines(), |
509 | - got.splitlines(), lineterm="") |
510 | - self.assertEquals(expected, got, "\n" + "\n".join(diff)) |
511 | + observed.splitlines(), lineterm="") |
512 | + self.assertEqual(expected, observed, "\n" + "\n".join(diff)) |
513 | |
514 | |
515 | -class ComboTest(ComboTestBase, mocker.MockerTestCase): |
516 | +class ComboTest(ComboTestBase): |
517 | |
518 | def test_parse_url_keeps_order(self): |
519 | """Parsing a combo loader URL returns an ordered list of filenames.""" |
520 | - self.assertEquals( |
521 | + self.assertEqual( |
522 | parse_url(("http://yui.yahooapis.com/combo?" |
523 | "3.0.0/build/yui/yui-min.js&" |
524 | "3.0.0/build/oop/oop-min.js&" |
525 | @@ -82,12 +87,23 @@ class ComboTest(ComboTestBase, mocker.MockerTestCase): |
526 | "** oop-min **", |
527 | "/* event-custom/event-custom-min.js */", |
528 | "** event-custom-min **")) |
529 | - self.assertEquals( |
530 | - "".join(combine_files(["yui/yui-min.js", |
531 | - "oop/oop-min.js", |
532 | - "event-custom/event-custom-min.js"], |
533 | - root=test_dir)).strip(), |
534 | - expected) |
535 | + self.assertEqual( |
536 | + b"".join(combine_files( |
537 | + ["yui/yui-min.js", |
538 | + "oop/oop-min.js", |
539 | + "event-custom/event-custom-min.js"], |
540 | + root=test_dir)).strip(), |
541 | + expected.encode("ascii")) |
542 | + |
543 | + def test_combine_files_yields_only_byte_strings(self): |
544 | + """ |
545 | + Combined files are always byte strings. |
546 | + """ |
547 | + test_dir = self.makeDir() |
548 | + self.makeSampleFile(test_dir, "one.bin", "\x00\x01") |
549 | + self.makeSampleFile(test_dir, "two.txt", "foobar") |
550 | + for part in combine_files(["one.txt", "two.txt"], root=test_dir): |
551 | + self.assertIsInstance(part, bytes) |
552 | |
553 | def test_combine_css_makes_relative_path(self): |
554 | """ |
555 | @@ -129,9 +145,10 @@ class ComboTest(ComboTestBase, mocker.MockerTestCase): |
556 | } |
557 | """ |
558 | self.assertTextEquals( |
559 | - "".join(combine_files(["widget/assets/skins/sam/widget.css", |
560 | - "editor/assets/skins/sam/editor.css"], |
561 | - root=test_dir)).strip(), |
562 | + b"".join(combine_files( |
563 | + ["widget/assets/skins/sam/widget.css", |
564 | + "editor/assets/skins/sam/editor.css"], |
565 | + root=test_dir)).strip(), |
566 | expected) |
567 | |
568 | def test_combine_css_leaves_absolute_urls_untouched(self): |
569 | @@ -174,9 +191,10 @@ class ComboTest(ComboTestBase, mocker.MockerTestCase): |
570 | } |
571 | """ |
572 | self.assertTextEquals( |
573 | - "".join(combine_files(["widget/assets/skins/sam/widget.css", |
574 | - "editor/assets/skins/sam/editor.css"], |
575 | - root=test_dir)).strip(), |
576 | + b"".join(combine_files( |
577 | + ["widget/assets/skins/sam/widget.css", |
578 | + "editor/assets/skins/sam/editor.css"], |
579 | + root=test_dir)).strip(), |
580 | expected) |
581 | |
582 | def test_combine_css_leaves_data_uris_untouched(self): |
583 | @@ -219,35 +237,10 @@ class ComboTest(ComboTestBase, mocker.MockerTestCase): |
584 | } |
585 | """ |
586 | self.assertTextEquals( |
587 | - "".join(combine_files(["widget/assets/skins/sam/widget.css", |
588 | - "editor/assets/skins/sam/editor.css"], |
589 | - root=test_dir)).strip(), |
590 | - expected) |
591 | - |
592 | - def test_missing_file_is_ignored(self): |
593 | - """If a missing file is requested we should still combine others.""" |
594 | - test_dir = self.makeDir() |
595 | - |
596 | - self.makeSampleFile( |
597 | - test_dir, |
598 | - os.path.join("yui", "yui-min.js"), |
599 | - "** yui-min **"), |
600 | - self.makeSampleFile( |
601 | - test_dir, |
602 | - os.path.join("event-custom", "event-custom-min.js"), |
603 | - "** event-custom-min **"), |
604 | - |
605 | - expected = "\n".join(("/* yui/yui-min.js */", |
606 | - "** yui-min **", |
607 | - "/* oop/oop-min.js */", |
608 | - "/* [missing] */", |
609 | - "/* event-custom/event-custom-min.js */", |
610 | - "** event-custom-min **")) |
611 | - self.assertEquals( |
612 | - "".join(combine_files(["yui/yui-min.js", |
613 | - "oop/oop-min.js", |
614 | - "event-custom/event-custom-min.js"], |
615 | - root=test_dir)).strip(), |
616 | + b"".join(combine_files( |
617 | + ["widget/assets/skins/sam/widget.css", |
618 | + "editor/assets/skins/sam/editor.css"], |
619 | + root=test_dir)).strip(), |
620 | expected) |
621 | |
622 | def test_no_parent_hack(self): |
623 | @@ -265,10 +258,9 @@ class ComboTest(ComboTestBase, mocker.MockerTestCase): |
624 | hack = "../../oop/oop-min.js" |
625 | self.assertTrue(os.path.exists(os.path.join(root, hack))) |
626 | |
627 | - expected = "\n".join(("/* ../../oop/oop-min.js */", |
628 | - "/* [missing] */")) |
629 | - self.assertEquals( |
630 | - "".join(combine_files([hack], root=root)).strip(), |
631 | + expected = b"" |
632 | + self.assertEqual( |
633 | + b"".join(combine_files([hack], root=root)).strip(), |
634 | expected) |
635 | |
636 | def test_no_absolute_path_hack(self): |
637 | @@ -281,9 +273,9 @@ class ComboTest(ComboTestBase, mocker.MockerTestCase): |
638 | hack = "/etc/passwd" |
639 | self.assertTrue(os.path.exists("/etc/passwd")) |
640 | |
641 | - expected = "/* /etc/passwd */\n/* [missing] */" |
642 | - self.assertEquals( |
643 | - "".join(combine_files([hack], root=test_dir)).strip(), |
644 | + expected = b"" |
645 | + self.assertEqual( |
646 | + b"".join(combine_files([hack], root=test_dir)).strip(), |
647 | expected) |
648 | |
649 | def test_no_traversing_out_of_root(self): |
650 | @@ -295,17 +287,17 @@ class ComboTest(ComboTestBase, mocker.MockerTestCase): |
651 | |
652 | hack = ".." |
653 | |
654 | - expected = "/* .. */\n/* [missing] */" |
655 | - self.assertEquals( |
656 | - "".join(combine_files([hack], root=test_dir)).strip(), |
657 | + expected = b"" |
658 | + self.assertEqual( |
659 | + b"".join(combine_files([hack], root=test_dir)).strip(), |
660 | expected) |
661 | |
662 | # from /tmp/somedir we want to try to walk up to / and into |
663 | # etc/password |
664 | hack = "../../etc/password" |
665 | - expected = "/* ../../etc/password */\n/* [missing] */" |
666 | - self.assertEquals( |
667 | - "".join(combine_files([hack], root=test_dir)).strip(), |
668 | + expected = b"" |
669 | + self.assertEqual( |
670 | + b"".join(combine_files([hack], root=test_dir)).strip(), |
671 | expected) |
672 | |
673 | def test_combine_css_adds_custom_prefix(self): |
674 | @@ -348,38 +340,89 @@ class ComboTest(ComboTestBase, mocker.MockerTestCase): |
675 | } |
676 | """ |
677 | self.assertTextEquals( |
678 | - "".join(combine_files(["widget/assets/skins/sam/widget.css", |
679 | - "editor/assets/skins/sam/editor.css"], |
680 | - root=test_dir, |
681 | - resource_prefix="/static/")).strip(), |
682 | + b"".join(combine_files( |
683 | + ["widget/assets/skins/sam/widget.css", |
684 | + "editor/assets/skins/sam/editor.css"], |
685 | + root=test_dir, resource_prefix="/static/")).strip(), |
686 | + expected) |
687 | + |
688 | + def test_combine_css_adds_custom_prefix_minified(self): |
689 | + """ |
690 | + The prefix is added to all url() declaration in CSS files, when the |
691 | + content of the .css file is minified. |
692 | + """ |
693 | + test_dir = self.makeDir() |
694 | + |
695 | + self.makeSampleFile( |
696 | + test_dir, |
697 | + os.path.join("path", "to", "widget.css"), |
698 | + ".foo{background:url(foo.png);}" |
699 | + ".bar{background:url(bar.png);}") |
700 | + expected = ( |
701 | + "/* path/to/widget.css */\n" |
702 | + ".foo{background:url(/static/path/to/foo.png);}" |
703 | + ".bar{background:url(/static/path/to/bar.png);}") |
704 | + self.assertTextEquals( |
705 | + b"".join(combine_files( |
706 | + ["path/to/widget.css"], root=test_dir, |
707 | + resource_prefix="/static/")), |
708 | expected) |
709 | |
710 | def test_rewrite_url_normalizes_parent_references(self): |
711 | """URL references in CSS files get normalized for parent dirs.""" |
712 | test_dir = self.makeDir() |
713 | - files = [ |
714 | - self.makeSampleFile( |
715 | - test_dir, |
716 | - os.path.join("yui", "base", "base.css"), |
717 | - ".foo{background-image:url(../../img.png)}"), |
718 | - ] |
719 | + self.makeSampleFile( |
720 | + test_dir, os.path.join("yui", "base", "base.css"), |
721 | + ".foo{background-image:url(../../img.png)}"), |
722 | |
723 | expected = """ |
724 | /* yui/base/base.css */ |
725 | .foo{background-image:url(img.png)} |
726 | """ |
727 | self.assertTextEquals( |
728 | - "".join(combine_files(["yui/base/base.css"], |
729 | - root=test_dir)).strip(), |
730 | + b"".join(combine_files( |
731 | + ["yui/base/base.css"], |
732 | + root=test_dir)).strip(), |
733 | expected) |
734 | |
735 | |
736 | -class WSGIComboTest(ComboTestBase, mocker.MockerTestCase): |
737 | +class ValidateFilesTest(ComboTestBase): |
738 | + |
739 | + def test_missing_file_is_ignored(self): |
740 | + """If a missing file is requested we should still combine others.""" |
741 | + test_dir = self.makeDir() |
742 | + |
743 | + self.makeSampleFile( |
744 | + test_dir, |
745 | + os.path.join("yui", "yui-min.js"), |
746 | + "** yui-min **"), |
747 | + self.makeSampleFile( |
748 | + test_dir, |
749 | + os.path.join("event", "event-min.js"), |
750 | + "** event-min **"), |
751 | + |
752 | + self.assertRaises( |
753 | + InvalidFileError, validate_files, |
754 | + ["yui/yui-min.js", "oop/oop-min.js", "event/event-min.js"], |
755 | + root=test_dir) |
756 | + |
757 | + |
758 | +class WSGIComboTest(ComboTestBase): |
759 | |
760 | def setUp(self): |
761 | self.root = self.makeDir() |
762 | self.app = TestApp(combo_app(self.root)) |
763 | |
764 | + def assertHeaders(self, observed, expected): |
765 | + """Ensure that the `expected` headers are present in `observed`. |
766 | + |
767 | + :param observed: A mapping of HTTP response headers. |
768 | + :param expected: A list of HTTP response headers (as 2-tuples). |
769 | + """ |
770 | + for name, value in expected: |
771 | + self.assertIn(name, observed) |
772 | + self.assertEqual(value, observed[name]) |
773 | + |
774 | def test_combo_app_sets_content_type_for_js(self): |
775 | """The WSGI App should set a proper Content-Type for Javascript.""" |
776 | self.makeSampleFile( |
777 | @@ -406,9 +449,11 @@ class WSGIComboTest(ComboTestBase, mocker.MockerTestCase): |
778 | ["yui/yui-min.js", |
779 | "oop/oop-min.js", |
780 | "event-custom/event-custom-min.js"]), status=200) |
781 | - self.assertEquals(res.headers, [("Content-Type", "text/javascript"), |
782 | - ("X-Content-Type-Options", "nosniff")]) |
783 | - self.assertEquals(res.body.strip(), expected) |
784 | + self.assertHeaders(res.headers, [ |
785 | + ("Content-Type", "text/javascript"), |
786 | + ("X-Content-Type-Options", "nosniff"), |
787 | + ]) |
788 | + self.assertEqual(res.body.strip(), expected.encode("ascii")) |
789 | |
790 | def test_combo_app_sets_content_type_for_css(self): |
791 | """The WSGI App should set a proper Content-Type for CSS.""" |
792 | @@ -421,27 +466,39 @@ class WSGIComboTest(ComboTestBase, mocker.MockerTestCase): |
793 | |
794 | res = self.app.get("/?" + "&".join( |
795 | ["widget/skin/sam/widget.css"]), status=200) |
796 | - self.assertEquals(res.headers, [("Content-Type", "text/css"), |
797 | - ("X-Content-Type-Options", "nosniff")]) |
798 | - self.assertEquals(res.body.strip(), expected) |
799 | + self.assertHeaders(res.headers, [ |
800 | + ("Content-Type", "text/css"), |
801 | + ("X-Content-Type-Options", "nosniff"), |
802 | + ]) |
803 | + self.assertEqual(res.body.strip(), expected.encode("ascii")) |
804 | |
805 | def test_no_filename_gives_404(self): |
806 | """If no filename is included, a 404 should be returned.""" |
807 | res = self.app.get("/", status=404) |
808 | - self.assertEquals(res.headers, [("Content-Type", "text/plain")]) |
809 | - self.assertEquals(res.body, "Not Found") |
810 | + self.assertHeaders(res.headers, [("Content-Type", "text/plain")]) |
811 | + self.assertEqual(res.body, b"Not Found") |
812 | |
813 | def test_bogus_filenames_are_plain_text_and_not_sniffed(self): |
814 | """ |
815 | Content-Type and X-Content-Type-Options headers set for |
816 | non-existent files. |
817 | """ |
818 | - res = self.app.get("/?" + "&".join( |
819 | - ["foo/bar/baz", |
820 | - "<html><script>alert(document.domain)</script></html>"]), |
821 | - status=200) |
822 | - self.assertEquals(res.headers, [("Content-Type", "text/plain"), |
823 | - ("X-Content-Type-Options", "nosniff")]) |
824 | + res = self.app.get( |
825 | + "/?" + "&".join([ |
826 | + "foo/bar/baz", |
827 | + "<html><script>alert(document.domain)</script></html>", |
828 | + ]), |
829 | + status=400) |
830 | + self.assertHeaders(res.headers, [ |
831 | + ("Content-Type", "text/plain"), |
832 | + ("X-Content-Type-Options", "nosniff"), |
833 | + ]) |
834 | + |
835 | + def test_js_comment_escape_hack(self): |
836 | + """Attacks to break out of the JS comment will get a 400 error.""" |
837 | + res = self.app.get("/?%s" % "*/alert%28%27owned%27%29/*", |
838 | + status=400) |
839 | + self.assertEqual(res.body.strip(), b"Bad Request") |
840 | |
841 | def test_combo_respects_path_hints(self): |
842 | """If I add path info into the combo url, convoy should use it.""" |
843 | @@ -476,13 +533,11 @@ class WSGIComboTest(ComboTestBase, mocker.MockerTestCase): |
844 | expected2 = "\n".join(("/* yui/yui-min.js */", |
845 | "/* yui-min-2 */")) |
846 | |
847 | - res = app.get("/%s/?%s" % (first_base, |
848 | - "yui/yui-min.js"), status=200) |
849 | - self.assertEquals(res.body.strip(), expected) |
850 | + res = app.get("/%s/?%s" % (first_base, "yui/yui-min.js"), status=200) |
851 | + self.assertEqual(res.body.strip(), expected.encode("ascii")) |
852 | |
853 | - res = app.get("/%s/?%s" % (second_base, "&".join( |
854 | - ["yui/yui-min.js"])), status=200) |
855 | - self.assertEquals(res.body.strip(), expected2) |
856 | + res = app.get("/%s/?%s" % (second_base, "yui/yui-min.js"), status=200) |
857 | + self.assertEqual(res.body.strip(), expected2.encode("ascii")) |
858 | |
859 | def test_path_hint_cant_break_root(self): |
860 | """You should not be able to get outside of the root via path hint.""" |
861 | @@ -492,6 +547,15 @@ class WSGIComboTest(ComboTestBase, mocker.MockerTestCase): |
862 | # and ask for passwd, it's inside the new adjusted root |
863 | app.get("/../etc?yui-min&passwd", status=400) |
864 | |
865 | - |
866 | -def test_suite(): |
867 | - return defaultTestLoader.loadTestsFromName(__name__) |
868 | + def test_cache_headers_set(self): |
869 | + app = TestApp(combo_app( |
870 | + self.root, additional_headers=[ |
871 | + ('Cache-Control', 'max-age=3600, public'), |
872 | + ])) |
873 | + res = app.get("/?" + "&".join( |
874 | + ["widget/skin/sam/widget.css"]), status=400) |
875 | + self.assertHeaders(res.headers, [ |
876 | + ("Content-Type", "text/css"), |
877 | + ("X-Content-Type-Options", "nosniff"), |
878 | + ('Cache-Control', 'max-age=3600, public'), |
879 | + ]) |
880 | diff --git a/convoy/tests/test_meta.py b/convoy/tests/test_meta.py |
881 | index 813af6a..1d9de2d 100644 |
882 | --- a/convoy/tests/test_meta.py |
883 | +++ b/convoy/tests/test_meta.py |
884 | @@ -1,5 +1,5 @@ |
885 | # Convoy is a WSGI app for loading multiple files in the same request. |
886 | -# Copyright (C) 2010-2012 Canonical, Ltd. |
887 | +# Copyright (C) 2011-2015 Canonical, Ltd. |
888 | # |
889 | # This program is free software: you can redistribute it and/or modify |
890 | # it under the terms of the GNU Affero General Public License as |
891 | @@ -16,11 +16,9 @@ |
892 | |
893 | |
894 | import os |
895 | -from unittest import defaultTestLoader, TestCase |
896 | - |
897 | -import mocker |
898 | |
899 | from convoy.meta import Builder, extract_metadata |
900 | +from convoy.tests import ConvoyTestCase |
901 | |
902 | |
903 | class TestBuilder(Builder): |
904 | @@ -30,12 +28,32 @@ class TestBuilder(Builder): |
905 | pass |
906 | |
907 | |
908 | -class ExtractMetadataTest(TestCase): |
909 | +class ExtractMetadataTest(ConvoyTestCase): |
910 | + |
911 | + def test_extract_with_comments(self): |
912 | + """ |
913 | + Extracting the metadata of a file containing comments |
914 | + in its requires block should still successfully |
915 | + extract its requirements. |
916 | + """ |
917 | + metadata = extract_metadata("""\ |
918 | + YUI.add('lazr.base', function(Y){ |
919 | + Y.log('Hello World'); |
920 | + }, '0.1', { |
921 | + "requires": [ |
922 | + // this is a comment |
923 | + "node", "base" // so is this |
924 | + ] |
925 | + }); |
926 | + """) |
927 | + self.assertEqual(len(metadata), 1) |
928 | + self.assertEqual(metadata[0]["name"], "lazr.base") |
929 | + self.assertEqual(metadata[0]["requires"], ["node", "base"]) |
930 | |
931 | def test_extract_single_module(self): |
932 | """ |
933 | Extracting the metadata of a file containing a single module |
934 | - should successfully extract it's requirements. |
935 | + should successfully extract its requirements. |
936 | """ |
937 | metadata = extract_metadata("""\ |
938 | YUI.add('lazr.base', function(Y){ |
939 | @@ -43,9 +61,9 @@ class ExtractMetadataTest(TestCase): |
940 | }, '0.1', {"requires": ["node", "base"]}); |
941 | """) |
942 | |
943 | - self.assertEquals(len(metadata), 1) |
944 | - self.assertEquals(metadata[0]["name"], "lazr.base") |
945 | - self.assertEquals(metadata[0]["requires"], ["node", "base"]) |
946 | + self.assertEqual(len(metadata), 1) |
947 | + self.assertEqual(metadata[0]["name"], "lazr.base") |
948 | + self.assertEqual(metadata[0]["requires"], ["node", "base"]) |
949 | |
950 | def test_extract_multiple_modules(self): |
951 | """ |
952 | @@ -61,13 +79,13 @@ class ExtractMetadataTest(TestCase): |
953 | }, '0.1', {"requires": ["node", "anim", "event"]}); |
954 | """) |
955 | |
956 | - self.assertEquals(len(metadata), 2) |
957 | + self.assertEqual(len(metadata), 2) |
958 | |
959 | - self.assertEquals(metadata[0]["name"], "lazr.base") |
960 | - self.assertEquals(metadata[0]["requires"], ["node", "base"]) |
961 | + self.assertEqual(metadata[0]["name"], "lazr.base") |
962 | + self.assertEqual(metadata[0]["requires"], ["node", "base"]) |
963 | |
964 | - self.assertEquals(metadata[1]["name"], "lazr.anim") |
965 | - self.assertEquals(metadata[1]["requires"], ["node", "anim", "event"]) |
966 | + self.assertEqual(metadata[1]["name"], "lazr.anim") |
967 | + self.assertEqual(metadata[1]["requires"], ["node", "anim", "event"]) |
968 | |
969 | def test_extract_multi_line(self): |
970 | """ |
971 | @@ -81,9 +99,9 @@ class ExtractMetadataTest(TestCase): |
972 | "event"]}); |
973 | """) |
974 | |
975 | - self.assertEquals(len(metadata), 1) |
976 | - self.assertEquals(metadata[0]["name"], "lazr.anim") |
977 | - self.assertEquals(metadata[0]["requires"], ["node", "anim", "event"]) |
978 | + self.assertEqual(len(metadata), 1) |
979 | + self.assertEqual(metadata[0]["name"], "lazr.anim") |
980 | + self.assertEqual(metadata[0]["requires"], ["node", "anim", "event"]) |
981 | |
982 | def test_extract_odd_spacing(self): |
983 | """ |
984 | @@ -96,9 +114,9 @@ class ExtractMetadataTest(TestCase): |
985 | }, '0.1', {"requires": [ "node" ,"anim" , "event" ]}); |
986 | """) |
987 | |
988 | - self.assertEquals(len(metadata), 1) |
989 | - self.assertEquals(metadata[0]["name"], "lazr.anim") |
990 | - self.assertEquals(metadata[0]["requires"], ["node", "anim", "event"]) |
991 | + self.assertEqual(len(metadata), 1) |
992 | + self.assertEqual(metadata[0]["name"], "lazr.anim") |
993 | + self.assertEqual(metadata[0]["requires"], ["node", "anim", "event"]) |
994 | |
995 | def test_extract_with_use(self): |
996 | """ |
997 | @@ -108,13 +126,14 @@ class ExtractMetadataTest(TestCase): |
998 | metadata = extract_metadata("""\ |
999 | YUI.add('lazr.anim', function(Y){ |
1000 | Y.log('Hello World'); |
1001 | - }, '0.1', {"use": ["dom"], "requires": [ "node" ,"anim" , "event" ]}); |
1002 | + }, '0.1', {"use": ["dom"], "requires": [ |
1003 | + "node" ,"anim" , "event" ]}); |
1004 | """) |
1005 | |
1006 | - self.assertEquals(len(metadata), 1) |
1007 | - self.assertEquals(metadata[0]["name"], "lazr.anim") |
1008 | - self.assertEquals(metadata[0]["requires"], ["node", "anim", "event"]) |
1009 | - self.assertEquals(metadata[0]["use"], ["dom"]) |
1010 | + self.assertEqual(len(metadata), 1) |
1011 | + self.assertEqual(metadata[0]["name"], "lazr.anim") |
1012 | + self.assertEqual(metadata[0]["requires"], ["node", "anim", "event"]) |
1013 | + self.assertEqual(metadata[0]["use"], ["dom"]) |
1014 | |
1015 | def test_extract_with_requires_before_use(self): |
1016 | """ |
1017 | @@ -123,13 +142,14 @@ class ExtractMetadataTest(TestCase): |
1018 | metadata = extract_metadata("""\ |
1019 | YUI.add('lazr.anim', function(Y){ |
1020 | Y.log('Hello World'); |
1021 | - }, '0.1', {"requires": [ "node" ,"anim" , "event" ], "use": ["dom"]}); |
1022 | + }, '0.1', {"requires": [ "node" ,"anim" , "event" ], |
1023 | + "use": ["dom"]}); |
1024 | """) |
1025 | |
1026 | - self.assertEquals(len(metadata), 1) |
1027 | - self.assertEquals(metadata[0]["name"], "lazr.anim") |
1028 | - self.assertEquals(metadata[0]["requires"], ["node", "anim", "event"]) |
1029 | - self.assertEquals(metadata[0]["use"], ["dom"]) |
1030 | + self.assertEqual(len(metadata), 1) |
1031 | + self.assertEqual(metadata[0]["name"], "lazr.anim") |
1032 | + self.assertEqual(metadata[0]["requires"], ["node", "anim", "event"]) |
1033 | + self.assertEqual(metadata[0]["use"], ["dom"]) |
1034 | |
1035 | def test_extract_requires_in_new_line(self): |
1036 | """ |
1037 | @@ -148,10 +168,10 @@ class ExtractMetadataTest(TestCase): |
1038 | ]}); |
1039 | """) |
1040 | |
1041 | - self.assertEquals(len(metadata), 1) |
1042 | - self.assertEquals(metadata[0]["name"], "lazr.anim") |
1043 | - self.assertEquals(metadata[0]["requires"], ["node", "anim", "event"]) |
1044 | - self.assertEquals(metadata[0]["use"], ["dom"]) |
1045 | + self.assertEqual(len(metadata), 1) |
1046 | + self.assertEqual(metadata[0]["name"], "lazr.anim") |
1047 | + self.assertEqual(metadata[0]["requires"], ["node", "anim", "event"]) |
1048 | + self.assertEqual(metadata[0]["use"], ["dom"]) |
1049 | |
1050 | def test_extract_metadata_not_metadata(self): |
1051 | """ |
1052 | @@ -159,11 +179,13 @@ class ExtractMetadataTest(TestCase): |
1053 | """ |
1054 | metadata = extract_metadata(""" |
1055 | YUI.add('test', function(Y) { |
1056 | - this._dds[h[i]] = new YAHOO.util.DragDrop(this._handles[h[i]], this.get('id') + '-handle-' + h, { useShim: this.get('useShim') }); |
1057 | + this._dds[h[i]] = new YAHOO.util.DragDrop( |
1058 | + this._handles[h[i]], this.get('id') + '-handle-' + h, { |
1059 | + useShim: this.get('useShim') }); |
1060 | }, '1.0', {requires: ['dom']}); |
1061 | """) |
1062 | |
1063 | - self.assertEquals(metadata[0]["requires"], ["dom"]) |
1064 | + self.assertEqual(metadata[0]["requires"], ["dom"]) |
1065 | |
1066 | def test_extract_metadata_single_quotes(self): |
1067 | """ |
1068 | @@ -182,11 +204,10 @@ class ExtractMetadataTest(TestCase): |
1069 | ]}); |
1070 | """) |
1071 | |
1072 | - self.assertEquals(len(metadata), 1) |
1073 | - self.assertEquals(metadata[0]["name"], "lazr.anim") |
1074 | - self.assertEquals(metadata[0]["requires"], ["node", "anim", "event"]) |
1075 | - self.assertEquals(metadata[0]["use"], ["dom"]) |
1076 | - |
1077 | + self.assertEqual(len(metadata), 1) |
1078 | + self.assertEqual(metadata[0]["name"], "lazr.anim") |
1079 | + self.assertEqual(metadata[0]["requires"], ["node", "anim", "event"]) |
1080 | + self.assertEqual(metadata[0]["use"], ["dom"]) |
1081 | |
1082 | def test_extract_has_no_quotes(self): |
1083 | """ |
1084 | @@ -205,10 +226,10 @@ class ExtractMetadataTest(TestCase): |
1085 | ]}); |
1086 | """) |
1087 | |
1088 | - self.assertEquals(len(metadata), 1) |
1089 | - self.assertEquals(metadata[0]["name"], "lazr.anim") |
1090 | - self.assertEquals(metadata[0]["requires"], ["node", "anim", "event"]) |
1091 | - self.assertEquals(metadata[0]["use"], ["dom"]) |
1092 | + self.assertEqual(len(metadata), 1) |
1093 | + self.assertEqual(metadata[0]["name"], "lazr.anim") |
1094 | + self.assertEqual(metadata[0]["requires"], ["node", "anim", "event"]) |
1095 | + self.assertEqual(metadata[0]["use"], ["dom"]) |
1096 | |
1097 | def test_extract_has_single_quotes(self): |
1098 | """ |
1099 | @@ -227,10 +248,10 @@ class ExtractMetadataTest(TestCase): |
1100 | ]}); |
1101 | """) |
1102 | |
1103 | - self.assertEquals(len(metadata), 1) |
1104 | - self.assertEquals(metadata[0]["name"], "lazr.anim") |
1105 | - self.assertEquals(metadata[0]["requires"], ["node", "anim", "event"]) |
1106 | - self.assertEquals(metadata[0]["use"], ["dom"]) |
1107 | + self.assertEqual(len(metadata), 1) |
1108 | + self.assertEqual(metadata[0]["name"], "lazr.anim") |
1109 | + self.assertEqual(metadata[0]["requires"], ["node", "anim", "event"]) |
1110 | + self.assertEqual(metadata[0]["use"], ["dom"]) |
1111 | |
1112 | def test_extract_has_use(self): |
1113 | """ |
1114 | @@ -250,10 +271,10 @@ class ExtractMetadataTest(TestCase): |
1115 | ]}); |
1116 | """) |
1117 | |
1118 | - self.assertEquals(len(metadata), 1) |
1119 | - self.assertEquals(metadata[0]["name"], "lazr.anim") |
1120 | - self.assertEquals(metadata[0]["requires"], ["node", "anim", "event"]) |
1121 | - self.assertEquals(metadata[0]["use"], ["cause", "dom"]) |
1122 | + self.assertEqual(len(metadata), 1) |
1123 | + self.assertEqual(metadata[0]["name"], "lazr.anim") |
1124 | + self.assertEqual(metadata[0]["requires"], ["node", "anim", "event"]) |
1125 | + self.assertEqual(metadata[0]["use"], ["cause", "dom"]) |
1126 | |
1127 | def test_extract_with_use_and_no_requires(self): |
1128 | """ |
1129 | @@ -265,9 +286,9 @@ class ExtractMetadataTest(TestCase): |
1130 | }, '0.1', {"use": ["dom"]}); |
1131 | """) |
1132 | |
1133 | - self.assertEquals(len(metadata), 1) |
1134 | - self.assertEquals(metadata[0]["name"], "lazr.anim") |
1135 | - self.assertEquals(metadata[0]["use"], ["dom"]) |
1136 | + self.assertEqual(len(metadata), 1) |
1137 | + self.assertEqual(metadata[0]["name"], "lazr.anim") |
1138 | + self.assertEqual(metadata[0]["use"], ["dom"]) |
1139 | |
1140 | def test_extract_with_all_options(self): |
1141 | """ |
1142 | @@ -286,15 +307,20 @@ class ExtractMetadataTest(TestCase): |
1143 | "after": ["lazr.base"]}); |
1144 | """) |
1145 | |
1146 | - self.assertEquals(len(metadata), 1) |
1147 | - self.assertEquals(metadata[0]["name"], "lazr.anim") |
1148 | - self.assertEquals(metadata[0]["use"], ["dom"]) |
1149 | - self.assertEquals(metadata[0]["omit"], ["nono"]) |
1150 | - self.assertEquals(metadata[0]["optional"], ["meh"]) |
1151 | - self.assertEquals(metadata[0]["supersedes"], ["old-anim"]) |
1152 | - self.assertEquals(metadata[0]["after"], ["lazr.base"]) |
1153 | + self.assertEqual(len(metadata), 1) |
1154 | + self.assertEqual(metadata[0]["name"], "lazr.anim") |
1155 | + self.assertEqual(metadata[0]["use"], ["dom"]) |
1156 | + self.assertEqual(metadata[0]["omit"], ["nono"]) |
1157 | + self.assertEqual(metadata[0]["optional"], ["meh"]) |
1158 | + self.assertEqual(metadata[0]["supersedes"], ["old-anim"]) |
1159 | + self.assertEqual(metadata[0]["after"], ["lazr.base"]) |
1160 | + |
1161 | |
1162 | -class GenerateMetadataTest(mocker.MockerTestCase): |
1163 | +class GenerateMetadataTest(ConvoyTestCase): |
1164 | + |
1165 | + def readFile(self, path): |
1166 | + with open(path, "r") as fd: |
1167 | + return fd.read() |
1168 | |
1169 | def test_generate_metadata_simple(self): |
1170 | """ |
1171 | @@ -314,10 +340,10 @@ class GenerateMetadataTest(mocker.MockerTestCase): |
1172 | output=output, exclude_regex="") |
1173 | builder.do_build() |
1174 | |
1175 | - got = open(output, "r").read() |
1176 | + got = self.readFile(output) |
1177 | prefix = got[:18] |
1178 | modules = "\n\n".join(got.split("\n\n")[1:-1]) |
1179 | - self.assertEquals(prefix, "var LAZR_CONFIG = ") |
1180 | + self.assertEqual(prefix, "var LAZR_CONFIG = ") |
1181 | self.assertTrue('[PATH] = "anim/anim-min.js"' in modules) |
1182 | self.assertFalse('[SKINNABLE] = FALSE' in modules) |
1183 | self.assertTrue('[TYPE] = JS' in modules) |
1184 | @@ -350,10 +376,10 @@ class GenerateMetadataTest(mocker.MockerTestCase): |
1185 | prefix="lazr") |
1186 | builder.do_build() |
1187 | |
1188 | - got = open(output, "r").read() |
1189 | + got = self.readFile(output) |
1190 | prefix = got[:18] |
1191 | modules = "\n\n".join(got.split("\n\n")[1:-1]) |
1192 | - self.assertEquals(prefix, "var LAZR_CONFIG = ") |
1193 | + self.assertEqual(prefix, "var LAZR_CONFIG = ") |
1194 | self.assertTrue( |
1195 | '[PATH] = PREFIX + ' |
1196 | '"/anim/assets/skins/sam/anim-skin.css"' in modules) |
1197 | @@ -392,10 +418,10 @@ class GenerateMetadataTest(mocker.MockerTestCase): |
1198 | prefix=None) |
1199 | builder.do_build() |
1200 | |
1201 | - got = open(output, "r").read() |
1202 | + got = self.readFile(output) |
1203 | prefix = got[:18] |
1204 | modules = "\n\n".join(got.split("\n\n")[1:-1]) |
1205 | - self.assertEquals(prefix, "var LAZR_CONFIG = ") |
1206 | + self.assertEqual(prefix, "var LAZR_CONFIG = ") |
1207 | self.assertFalse(' PREFIX =' in got, got) |
1208 | self.assertTrue( |
1209 | '[PATH] = "anim/assets/skins/sam/anim-skin.css"' in modules) |
1210 | @@ -429,10 +455,10 @@ class GenerateMetadataTest(mocker.MockerTestCase): |
1211 | prefix=None) |
1212 | builder.do_build() |
1213 | |
1214 | - got = open(output, "r").read() |
1215 | + got = self.readFile(output) |
1216 | prefix = got[:18] |
1217 | modules = "\n\n".join(got.split("\n\n")[1:-1]) |
1218 | - self.assertEquals(prefix, "var LAZR_CONFIG = ") |
1219 | + self.assertEqual(prefix, "var LAZR_CONFIG = ") |
1220 | self.assertFalse(' PREFIX =' in got, got) |
1221 | self.assertTrue( |
1222 | '[PATH] = "anim-min.js"' in modules) |
1223 | @@ -474,11 +500,11 @@ class GenerateMetadataTest(mocker.MockerTestCase): |
1224 | prefix="lazr") |
1225 | builder.do_build() |
1226 | |
1227 | - got = open(output, "r").read() |
1228 | + got = self.readFile(output) |
1229 | prefix = got[:18] |
1230 | blocks = got.split("\n\n") |
1231 | modules = "\n\n".join(blocks[1:-1]) |
1232 | - self.assertEquals(prefix, "var LAZR_CONFIG = ") |
1233 | + self.assertEqual(prefix, "var LAZR_CONFIG = ") |
1234 | |
1235 | self.assertTrue( |
1236 | '[PATH] = PREFIX + "/anim/assets/purty-anim-core.css"' in modules) |
1237 | @@ -527,7 +553,7 @@ class GenerateMetadataTest(mocker.MockerTestCase): |
1238 | prefix="lazr") |
1239 | builder.do_build() |
1240 | |
1241 | - got = open(output, "r").read() |
1242 | + got = self.readFile(output) |
1243 | self.assertIn('AFTER = "after",', got) |
1244 | self.assertIn('EXT = "ext",', got) |
1245 | self.assertIn('OPTIONAL = "optional",', got) |
1246 | @@ -536,7 +562,3 @@ class GenerateMetadataTest(mocker.MockerTestCase): |
1247 | self.assertIn('SKINNABLE = "skinnable",', got) |
1248 | self.assertIn('SUPERSEDES = "supersedes",', got) |
1249 | self.assertIn('USE = "use",', got) |
1250 | - |
1251 | - |
1252 | -def test_suite(): |
1253 | - return defaultTestLoader.loadTestsFromName(__name__) |
1254 | diff --git a/debian/changelog b/debian/changelog |
1255 | index 4f051e9..96c8e67 100644 |
1256 | --- a/debian/changelog |
1257 | +++ b/debian/changelog |
1258 | @@ -1,3 +1,25 @@ |
1259 | +convoy (0.2.1+bzr39-1ubuntu1) focal; urgency=medium |
1260 | + |
1261 | + * Drop python2 package in favor of python3. |
1262 | + - Change autopkgtest to use python3 dependencies. Drop dependence on |
1263 | + python-mocker since there's no py3 version of it; the testsuite |
1264 | + doesn't appear to need it when run under python3. |
1265 | + |
1266 | + -- Bryce Harrington <bryce@canonical.com> Wed, 08 Jan 2020 15:42:58 -0800 |
1267 | + |
1268 | +convoy (0.2.1+bzr39-1build1) focal; urgency=medium |
1269 | + |
1270 | + * No-change rebuild to generate dependencies on python2. |
1271 | + |
1272 | + -- Matthias Klose <doko@ubuntu.com> Tue, 17 Dec 2019 12:31:52 +0000 |
1273 | + |
1274 | +convoy (0.2.1+bzr39-1) xenial; urgency=medium |
1275 | + |
1276 | + * New upstream snapshot. |
1277 | + * Build with python3. |
1278 | + |
1279 | + -- Andres Rodriguez <andreserl@ubuntu.com> Thu, 12 Nov 2015 16:26:56 +0000 |
1280 | + |
1281 | convoy (0.2.1+bzr25-3) unstable; urgency=low |
1282 | |
1283 | * Add missing autopkgtest dependencies on python-mocker and python- |
1284 | diff --git a/debian/control b/debian/control |
1285 | index fb59b84..cb70840 100644 |
1286 | --- a/debian/control |
1287 | +++ b/debian/control |
1288 | @@ -1,21 +1,24 @@ |
1289 | Source: convoy |
1290 | Section: python |
1291 | Priority: extra |
1292 | -Maintainer: Jelmer Vernooij <jelmer@debian.org> |
1293 | -Build-Depends: debhelper (>= 7.0.50~), |
1294 | - python-all (>= 2.6.6-3~), |
1295 | - python-mocker, |
1296 | - python-nose, |
1297 | - python-paste, |
1298 | - python-setuptools |
1299 | -Standards-Version: 3.9.5 |
1300 | +Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com> |
1301 | +XSBC-Original-Maintainer: Jelmer Vernooij <jelmer@debian.org> |
1302 | +Build-Depends: debhelper (>= 9~), |
1303 | + dh-python, |
1304 | + python3-all, |
1305 | + python3-nose, |
1306 | + python3-paste, |
1307 | + python3-setuptools, |
1308 | + python3-webtest |
1309 | +Standards-Version: 3.9.6 |
1310 | Homepage: https://launchpad.net/convoy |
1311 | +X-Python3-Version: >= 3.4 |
1312 | XS-Testsuite: autopkgtest |
1313 | |
1314 | -Package: python-convoy |
1315 | +Package: python3-convoy |
1316 | Architecture: all |
1317 | -Depends: ${misc:Depends}, ${python:Depends} |
1318 | -Description: WSGI app for loading multiple files in the same request |
1319 | +Depends: ${misc:Depends}, ${python3:Depends} |
1320 | +Description: WSGI app for loading multiple files in the same request (Python 3) |
1321 | Provides a WSGI application that can be hooked up to a |
1322 | generic WSGI container to create a combo loader server, for |
1323 | loading Javascript and CSS files combined into a single |
1324 | diff --git a/debian/python-convoy.install b/debian/python-convoy.install |
1325 | new file mode 100644 |
1326 | index 0000000..6a53a3b |
1327 | --- /dev/null |
1328 | +++ b/debian/python-convoy.install |
1329 | @@ -0,0 +1 @@ |
1330 | +usr/lib/python2*/*-packages/* |
1331 | diff --git a/debian/python3-convoy.install b/debian/python3-convoy.install |
1332 | new file mode 100644 |
1333 | index 0000000..87e0b0a |
1334 | --- /dev/null |
1335 | +++ b/debian/python3-convoy.install |
1336 | @@ -0,0 +1 @@ |
1337 | +usr/lib/python3/*-packages/* |
1338 | diff --git a/debian/rules b/debian/rules |
1339 | index 236638b..2d4f261 100755 |
1340 | --- a/debian/rules |
1341 | +++ b/debian/rules |
1342 | @@ -1,11 +1,22 @@ |
1343 | #!/usr/bin/make -f |
1344 | +export PYBUILD_NAME=convoy |
1345 | + |
1346 | +PYVERS := $(shell py3versions -r) |
1347 | |
1348 | %: |
1349 | - dh $@ --buildsystem=python_distutils --with python2 |
1350 | + dh $@ --with python3 --buildsystem=pybuild |
1351 | + |
1352 | +override_dh_auto_install: |
1353 | + dh_auto_install |
1354 | + set -ex; for python in $(PYVERS); do \ |
1355 | + $$python setup.py install --root=$(CURDIR)/debian/tmp --install-layout=deb; \ |
1356 | + done |
1357 | |
1358 | override_dh_auto_test: |
1359 | ifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS))) |
1360 | - python$* setup.py test |
1361 | + set -ex; for python in $(PYVERS); do \ |
1362 | + $$python setup.py test; \ |
1363 | + done |
1364 | endif |
1365 | |
1366 | |
1367 | diff --git a/debian/tests/control b/debian/tests/control |
1368 | index ddb7fae..e2c7467 100644 |
1369 | --- a/debian/tests/control |
1370 | +++ b/debian/tests/control |
1371 | @@ -1,3 +1,3 @@ |
1372 | Tests: testsuite |
1373 | -Depends: python-convoy, python-mocker, python-paste |
1374 | +Depends: python3-convoy, python3-paste |
1375 | Restrictions: allow-stderr |
1376 | diff --git a/debian/tests/testsuite b/debian/tests/testsuite |
1377 | index b1846e0..de41ca5 100644 |
1378 | --- a/debian/tests/testsuite |
1379 | +++ b/debian/tests/testsuite |
1380 | @@ -1,2 +1,2 @@ |
1381 | #!/bin/sh -ex |
1382 | -python -m unittest discover convoy/tests |
1383 | +python3 -m unittest discover convoy/tests |
1384 | diff --git a/setup.py b/setup.py |
1385 | index c6c4be3..aec07a0 100644 |
1386 | --- a/setup.py |
1387 | +++ b/setup.py |
1388 | @@ -1,5 +1,5 @@ |
1389 | # Convoy is a WSGI app for loading multiple files in the same request. |
1390 | -# Copyright (C) 2010-2012 Canonical, Ltd. |
1391 | +# Copyright (C) 2011-2015 Canonical, Ltd. |
1392 | # |
1393 | # This program is free software: you can redistribute it and/or modify |
1394 | # it under the terms of the GNU Affero General Public License as |
1395 | @@ -14,43 +14,20 @@ |
1396 | # You should have received a copy of the GNU Affero General Public License |
1397 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
1398 | |
1399 | +from setuptools import setup |
1400 | |
1401 | -import os |
1402 | -from distutils.core import setup |
1403 | - |
1404 | -# If setuptools is present, use it to find_packages(), and also |
1405 | -# declare our dependency on epsilon. |
1406 | -extra_setup_args = {} |
1407 | -try: |
1408 | - from setuptools import find_packages |
1409 | - extra_setup_args = { |
1410 | - 'install_requires': ['Paste'], |
1411 | - 'tests_require': ['nose', 'mocker'] |
1412 | - } |
1413 | -except ImportError: |
1414 | - def find_packages(exclude=None): |
1415 | - """ |
1416 | - Compatibility wrapper. |
1417 | - |
1418 | - Taken from storm setup.py. |
1419 | - """ |
1420 | - packages = [] |
1421 | - for directory, subdirectories, files in os.walk("convoy"): |
1422 | - if '__init__.py' in files: |
1423 | - packages.append(directory.replace(os.sep, '.')) |
1424 | - return packages |
1425 | |
1426 | setup( |
1427 | name="convoy", |
1428 | - version="0.2.4", |
1429 | - description="A combo WSGI application for use with YUI", |
1430 | + version="0.4.4", |
1431 | + description="A combo-loader for Javascript and CSS.", |
1432 | author="Canonical Javascripters", |
1433 | + author_email="-", |
1434 | url="https://launchpad.net/convoy", |
1435 | license="AGPL", |
1436 | - packages=find_packages(exclude=('convoy.tests',)), |
1437 | - include_package_data=True, |
1438 | - zip_safe=False, |
1439 | - test_suite="nose.collector", |
1440 | + packages=['convoy'], |
1441 | + test_suite="convoy.tests", |
1442 | + tests_require=['nose', 'webtest'], |
1443 | classifiers=[ |
1444 | "Development Status :: 5 - Production/Stable", |
1445 | "Environment :: Web Environment", |
1446 | @@ -58,8 +35,8 @@ setup( |
1447 | "Intended Audience :: Information Technology", |
1448 | "License :: OSI Approved :: GNU Affero General Public License v3", |
1449 | "Programming Language :: Python", |
1450 | + "Programming Language :: Python :: 2", |
1451 | + "Programming Language :: Python :: 3", |
1452 | "Topic :: Internet :: WWW/HTTP", |
1453 | - ], |
1454 | - **extra_setup_args |
1455 | - ) |
1456 | - |
1457 | + ], |
1458 | +) |
1459 | diff --git a/tox.ini b/tox.ini |
1460 | new file mode 100644 |
1461 | index 0000000..e72377d |
1462 | --- /dev/null |
1463 | +++ b/tox.ini |
1464 | @@ -0,0 +1,5 @@ |
1465 | +[tox] |
1466 | +envlist = py27, py34, py35 |
1467 | + |
1468 | +[testenv] |
1469 | +commands = {envbindir}/python {toxinidir}/setup.py test |
Btw, launchpad seems to be including the changes from 0.2.1+bzr39-1 for some reason. Here is a more minimal debdiff:
http:// paste.ubuntu. com/p/r2Qff7zzw b/