Merge lp:~gocept/landscape-client/py3-twisted-util into lp:~landscape/landscape-client/trunk

Proposed by Steffen Allner
Status: Superseded
Proposed branch: lp:~gocept/landscape-client/py3-twisted-util
Merge into: lp:~landscape/landscape-client/trunk
Prerequisite: lp:~gocept/landscape-client/python3
Diff against target: 422 lines (+101/-62)
8 files modified
landscape/broker/tests/test_store.py (+2/-2)
landscape/lib/fs.py (+37/-19)
landscape/lib/juju.py (+1/-1)
landscape/lib/tests/test_fs.py (+27/-5)
landscape/lib/tests/test_process.py (+5/-3)
landscape/lib/tests/test_twisted_util.py (+16/-20)
landscape/lib/twisted_util.py (+9/-9)
landscape/lib/vm_info.py (+4/-3)
To merge this branch: bzr merge lp:~gocept/landscape-client/py3-twisted-util
Reviewer Review Type Date Requested Status
🤖 Landscape Builder test results Approve
Steffen Allner (community) Needs Fixing
Daniel Havlik (community) Approve
Данило Шеган (community) Approve
Adam Collard (community) Abstain
Landscape Pending
Review via email: mp+319100@code.launchpad.net

This proposal has been superseded by a proposal from 2017-03-13.

Description of the change

We are on the journey to get the Bytes / String story solved for Python 2/3 compatibility.

This MP considers the part of code which receives data from the stdout and stderr and therefore handles with Bytes and not with Stings in Python 3. To reduce the amount of compat code, the code has been changed to use the same `io.BytesIO()` in Python 2 and 3.

To post a comment you must log in.
Revision history for this message
🤖 Landscape Builder (landscape-builder) :
review: Abstain (executing tests)
Revision history for this message
🤖 Landscape Builder (landscape-builder) wrote :

Command: TRIAL_ARGS=-j4 make check
Result: Fail
Revno: 959
Branch: lp:~gocept/landscape-client/py3-twisted-util
Jenkins: https://ci.lscape.net/job/latch-test-precise/887/

review: Needs Fixing (test results)
Revision history for this message
Adam Collard (adam-collard) :
review: Abstain
Revision history for this message
David Britton (dpb) wrote :

Hi Daniel -- Please add 'landscape' on these in the future. Otherwise only Adam will be able to review. :)

I've taken care of this one.

Thanks!

Revision history for this message
Данило Шеган (danilo) wrote :

I've just noticed that the dependency branch is still missing the second Landscape dev reviewer: I've added myself there and will be reviewing that first. We also require Adam to approve the changes there too.

I'll get back to this after I am done with that branch.

Revision history for this message
Данило Шеган (danilo) wrote :

FWIW, you should fix the test failure as well: http://paste.ubuntu.com/24136946/

(test builder is still running on Ubuntu 12.04 [precise], so maybe that's to blame, but I haven't looked deeper: if it's easy, it's nice to get a fix for that too, but if not, we can probably move to supporting only 14.04 and later since 12.04 is EOLin this April)

Revision history for this message
Steffen Allner (sallner) wrote :

I am currently developing on an Ubuntu 16.04.1 and cannot reproduce this test failure. But I agree, we should at least test it on 14.04.

Revision history for this message
Steffen Allner (sallner) wrote :

I am actually surprised that there should be only one test failure. I tried to test it on an Ubuntu 12.04. and 14.04. and failed both times. For 12.04 I already failed to install bzr as python-bzrlib depends on python-configobj, which in turn is a virtual-package now and did not install anything.

On 14.04. executing the tests led to some ImportErrors out of `twisted.python.compat` as some of the compat code we took is not available in twisted 13.2 (which is in the repos in 14.04.). For 12.04. twisted 11.1 is in the repos with even less compat code. So this is a bit unclear to me. Or is the landscape-builder using any kind of virtualenv to do the testing?

Revision history for this message
🤖 Landscape Builder (landscape-builder) :
review: Abstain (executing tests)
Revision history for this message
Данило Шеган (danilo) wrote :

Yeah, we are definitely missing some development setup bits for landscape-client: I've done an "apt-get build-dep landscape-client", and as you noted, bzr was still missing, but also python-mock and xvfb. xvfb is probably not really needed anymore with our GUI component retired, but I'll need to double check that. However, the problem with python-mock on 12.04 is that it seems to be very old (0.7.2) and the tests already use features from a newer version (1.0.1, eg. by adding "deb http://ubuntu-cloud.archive.canonical.com/ubuntu precise-updates/cloud-tools main" to your /etc/apt/sources.list). Updating python-twisted-core to the version from that archive (13.02) also made trial accept -j4 parameter so "make check" fully passed for me. Perhaps the failed test is failing intermittently: if so, I'll file a separate bug about that (retrying the test run right now).

I'll now look if 14.04 as-is has all the deps needed at sufficiently new versions and update our infrastructure to run the tests on that instead.

Revision history for this message
🤖 Landscape Builder (landscape-builder) wrote :

Command: TRIAL_ARGS=-j4 make check
Result: Success
Revno: 959
Branch: lp:~gocept/landscape-client/py3-twisted-util
Jenkins: https://ci.lscape.net/job/latch-test-precise/893/

review: Approve (test results)
Revision history for this message
Steffen Allner (sallner) wrote :

At least that one worked. Thanks, Danilo! Apart from that, I recall that we agreed on compatibility with 16.04. for the new version of landscape client. So if those problems persist or occur more frequently, we have to find a different mode.

Revision history for this message
Данило Шеган (danilo) wrote :

Righto: please do focus on what the agreement specifies, we can deal with compatibility in that case (sorry if I overstepped).

Revision history for this message
🤖 Landscape Builder (landscape-builder) :
review: Abstain (executing tests)
Revision history for this message
🤖 Landscape Builder (landscape-builder) wrote :

Command: TRIAL_ARGS=-j4 make check
Result: Success
Revno: 959
Branch: lp:~gocept/landscape-client/py3-twisted-util
Jenkins: https://ci.lscape.net/job/latch-test-xenial/1458/

review: Approve (test results)
Revision history for this message
🤖 Landscape Builder (landscape-builder) :
review: Abstain (executing tests)
Revision history for this message
🤖 Landscape Builder (landscape-builder) wrote :

Command: TRIAL_ARGS=-j4 make check
Result: Success
Revno: 959
Branch: lp:~gocept/landscape-client/py3-twisted-util
Jenkins: https://ci.lscape.net/job/latch-test-xenial/1459/

review: Approve (test results)
Revision history for this message
Данило Шеган (danilo) wrote :

Looks good, +1 (we do need to wait for the prereq to land, of course)

review: Approve
Revision history for this message
Daniel Havlik (nilo) wrote :

+1

review: Approve
Revision history for this message
Steffen Allner (sallner) wrote :

If we merge the py3-fs branch first, we will get some failing tests. So I will merge py3-fs into this branch and update the proposal to have it as prerequisite.

review: Needs Fixing
960. By Steffen Allner

Backmerge from trunk.

961. By Steffen Allner

Merge landscape.lib.fs with optional encoding.

962. By Steffen Allner

Use bytes here as we do not have non-ascii characters in the scripts.

Revision history for this message
🤖 Landscape Builder (landscape-builder) :
review: Abstain (executing tests)
Revision history for this message
🤖 Landscape Builder (landscape-builder) wrote :

Command: TRIAL_ARGS=-j4 make check
Result: Success
Revno: 962
Branch: lp:~gocept/landscape-client/py3-twisted-util
Jenkins: https://ci.lscape.net/job/latch-test-xenial/3605/

review: Approve (test results)
963. By Steffen Allner

Merge in current l.l.fs and use create_text_file().

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'landscape/broker/tests/test_store.py'
2--- landscape/broker/tests/test_store.py 2016-08-17 20:31:48 +0000
3+++ landscape/broker/tests/test_store.py 2017-03-13 10:45:51 +0000
4@@ -280,7 +280,7 @@
5 self.store.add({"type": "data", "data": 1})
6 # We simulate it by creating a fake file which raises halfway through
7 # writing a file.
8- with mock.patch("__builtin__.open") as mock_open:
9+ with mock.patch("landscape.lib.fs.open") as mock_open:
10 mocked_file = mock_open.return_value
11 mocked_file.write.side_effect = IOError("Sorry, pal!")
12 # This kind of ensures that raising an exception is somewhat
13@@ -288,7 +288,7 @@
14 # on special exception-handling in the file-writing code.
15 self.assertRaises(
16 IOError, self.store.add, {"type": "data", "data": 2})
17- mock_open.assert_called_with(mock.ANY, "w")
18+ mock_open.assert_called_with(mock.ANY, "wb")
19 mocked_file.write.assert_called_once_with(mock.ANY)
20 self.assertEqual(self.store.get_pending_messages(),
21 [{"type": "data", "data": 1, "api": "3.2"}])
22
23=== modified file 'landscape/lib/fs.py'
24--- landscape/lib/fs.py 2013-05-28 08:48:31 +0000
25+++ landscape/lib/fs.py 2017-03-13 10:45:51 +0000
26@@ -1,18 +1,31 @@
27 """File-system utils"""
28
29+import codecs
30 import os
31 import time
32
33
34-def create_file(path, content):
35+from twisted.python.compat import long
36+
37+
38+def create_file(path, content, encoding=None):
39 """Create a file with the given content.
40
41 @param path: The path to the file.
42 @param content: The content to be written in the file.
43+ @param encoding: An optional encoding. If set, the content will be encoded
44+ with C{encoding} before writing to the file.
45 """
46- fd = open(path, "w")
47- fd.write(content)
48- fd.close()
49+ if encoding:
50+ content = codecs.encode(content, encoding)
51+ # XXX: Due to a very specific mock of `open()` in landscape.broker.tests.\
52+ # test_store.MessageStoreTest.test_atomic_message_writing it is hard to
53+ # write this file opening as context manager.
54+ fd = open(path, "wb")
55+ try:
56+ fd.write(content)
57+ finally:
58+ fd.close()
59
60
61 def append_file(path, content):
62@@ -23,28 +36,33 @@
63 @param path: The path to the file.
64 @param content: The content to be written in the file at the end.
65 """
66- fd = open(path, "a")
67- fd.write(content)
68- fd.close()
69-
70-
71-def read_file(path, limit=None):
72+ with open(path, "a") as fd:
73+ fd.write(content)
74+
75+
76+def read_file(path, limit=None, encoding=None):
77 """Return the content of the given file.
78
79 @param path: The path to the file.
80 @param limit: An optional read limit. If positive, read up to that number
81 of bytes from the beginning of the file. If negative, read up to that
82 number of bytes from the end of the file.
83- @return content: The content of the file, possibly trimmed to C{limit}.
84+ @param encoding: An optional encoding. If set, the content will be returned
85+ decoded with C{encoding}.
86+ @return content: The content of the file as bytes or unicode string
87+ (depending on C{encoding}), possibly trimmed to C{limit}.
88 """
89- fd = open(path, "r")
90- if limit and os.path.getsize(path) > abs(limit):
91- whence = 0
92- if limit < 0:
93- whence = 2
94- fd.seek(limit, whence)
95- content = fd.read()
96- fd.close()
97+ # Use binary mode since opening a file in text mode in Python 3 does not
98+ # allow non-zero offset seek from the end of the file.
99+ with open(path, "rb") as fd:
100+ if limit and os.path.getsize(path) > abs(limit):
101+ whence = 0
102+ if limit < 0:
103+ whence = 2
104+ fd.seek(limit, whence)
105+ content = fd.read()
106+ if encoding:
107+ content = codecs.decode(content, encoding)
108 return content
109
110
111
112=== modified file 'landscape/lib/juju.py'
113--- landscape/lib/juju.py 2014-09-24 08:09:55 +0000
114+++ landscape/lib/juju.py 2017-03-13 10:45:51 +0000
115@@ -13,7 +13,7 @@
116 if not os.path.exists(config.juju_filename):
117 return
118
119- json_contents = read_file(config.juju_filename)
120+ json_contents = read_file(config.juju_filename, encoding="utf-8")
121 try:
122 juju_info = json.loads(json_contents)
123 # Catch any error the json lib could throw, because we don't know or
124
125=== modified file 'landscape/lib/tests/test_fs.py'
126--- landscape/lib/tests/test_fs.py 2016-06-15 16:42:17 +0000
127+++ landscape/lib/tests/test_fs.py 2017-03-13 10:45:51 +0000
128@@ -1,7 +1,11 @@
129+# -*- coding: utf-8 -*-
130+import codecs
131 import os
132 from mock import patch
133 import time
134
135+from twisted.python.compat import long
136+
137 from landscape.tests.helpers import LandscapeTest
138
139 from landscape.lib.fs import append_file, read_file, touch_file
140@@ -14,7 +18,16 @@
141 With no options L{read_file} reads the whole file passed as argument.
142 """
143 path = self.makeFile("foo")
144- self.assertEqual(read_file(path), "foo")
145+ self.assertEqual(read_file(path), b"foo")
146+
147+ def test_read_file_encoding(self):
148+ """
149+ With encoding L{read_file} reads the whole file passed as argument and
150+ decodes it.
151+ """
152+ utf8_content = codecs.encode(u"foo \N{SNOWMAN}", "utf-8")
153+ path = self.makeFile(utf8_content, mode="wb")
154+ self.assertEqual(read_file(path, encoding="utf-8"), u"foo ☃")
155
156 def test_read_file_with_limit(self):
157 """
158@@ -22,14 +35,23 @@
159 given limit.
160 """
161 path = self.makeFile("foo bar")
162- self.assertEqual(read_file(path, limit=3), " bar")
163+ self.assertEqual(read_file(path, limit=3), b" bar")
164+
165+ def test_read_file_with_limit_encoding(self):
166+ """
167+ With a positive limit L{read_file} reads only the bytes after the
168+ given limit.
169+ """
170+ utf8_content = codecs.encode(u"foo \N{SNOWMAN}", "utf-8")
171+ path = self.makeFile(utf8_content, mode="wb")
172+ self.assertEqual(read_file(path, limit=3, encoding="utf-8"), u" ☃")
173
174 def test_read_file_with_negative_limit(self):
175 """
176 With a negative limit L{read_file} reads only the tail of the file.
177 """
178 path = self.makeFile("foo bar from end")
179- self.assertEqual(read_file(path, limit=-3), "end")
180+ self.assertEqual(read_file(path, limit=-3), b"end")
181
182 def test_read_file_with_limit_bigger_than_file(self):
183 """
184@@ -37,8 +59,8 @@
185 file.
186 """
187 path = self.makeFile("foo bar from end")
188- self.assertEqual(read_file(path, limit=100), "foo bar from end")
189- self.assertEqual(read_file(path, limit=-100), "foo bar from end")
190+ self.assertEqual(read_file(path, limit=100), b"foo bar from end")
191+ self.assertEqual(read_file(path, limit=-100), b"foo bar from end")
192
193
194 class TouchFileTest(LandscapeTest):
195
196=== modified file 'landscape/lib/tests/test_process.py'
197--- landscape/lib/tests/test_process.py 2017-03-09 10:13:30 +0000
198+++ landscape/lib/tests/test_process.py 2017-03-13 10:45:51 +0000
199@@ -25,7 +25,8 @@
200 os.mkdir(process_dir)
201
202 cmd_line = "/usr/bin/foo"
203- create_file(os.path.join(process_dir, "cmdline"), cmd_line)
204+ create_file(
205+ os.path.join(process_dir, "cmdline"), cmd_line, encoding="utf-8")
206
207 status = "\n".join([
208 "Name: foo",
209@@ -34,11 +35,12 @@
210 "Gid: 2000",
211 "VmSize: 3000",
212 "Ignored: value"])
213- create_file(os.path.join(process_dir, "status"), status)
214+ create_file(
215+ os.path.join(process_dir, "status"), status, encoding="utf-8")
216
217 stat_array = [str(index) for index in range(44)]
218 stat = " ".join(stat_array)
219- create_file(os.path.join(process_dir, "stat"), stat)
220+ create_file(os.path.join(process_dir, "stat"), stat, encoding="utf-8")
221
222 @mock.patch("landscape.lib.process.detect_jiffies", return_value=1)
223 @mock.patch("os.listdir")
224
225=== modified file 'landscape/lib/tests/test_twisted_util.py'
226--- landscape/lib/tests/test_twisted_util.py 2017-01-23 12:14:33 +0000
227+++ landscape/lib/tests/test_twisted_util.py 2017-03-13 10:45:51 +0000
228@@ -1,8 +1,5 @@
229 import os
230
231-from twisted.python.compat import _PY3
232-from unittest import skipIf
233-
234 from landscape.lib.fs import create_file
235 from landscape.lib.twisted_util import spawn_process
236 from landscape.tests.helpers import LandscapeTest
237@@ -19,12 +16,12 @@
238 """
239 The process is executed and returns the expected exit code.
240 """
241- create_file(self.command, "#!/bin/sh\nexit 2")
242+ create_file(self.command, b"#!/bin/sh\nexit 2")
243
244 def callback(args):
245 out, err, code = args
246- self.assertEqual(out, "")
247- self.assertEqual(err, "")
248+ self.assertEqual(out, b"")
249+ self.assertEqual(err, b"")
250 self.assertEqual(code, 2)
251
252 result = spawn_process(self.command)
253@@ -37,8 +34,8 @@
254 """
255 def callback(args):
256 out, err, code = args
257- self.assertEqual(out, "a b")
258- self.assertEqual(err, "")
259+ self.assertEqual(out, b"a b")
260+ self.assertEqual(err, b"")
261 self.assertEqual(code, 0)
262
263 result = spawn_process(self.command, args=("a", "b"))
264@@ -49,12 +46,12 @@
265 """
266 The process returns the expected standard error.
267 """
268- create_file(self.command, "#!/bin/sh\necho -n $@ >&2")
269+ create_file(self.command, b"#!/bin/sh\necho -n $@ >&2")
270
271 def callback(args):
272 out, err, code = args
273- self.assertEqual(out, "")
274- self.assertEqual(err, "a b")
275+ self.assertEqual(out, b"")
276+ self.assertEqual(err, b"a b")
277 self.assertEqual(code, 0)
278
279 result = spawn_process(self.command, args=("a", "b"))
280@@ -66,9 +63,9 @@
281 If a callback for process output is provieded, it is called for every
282 line of output.
283 """
284- create_file(self.command, "#!/bin/sh\n/bin/echo -ne $@")
285+ create_file(self.command, b"#!/bin/sh\n/bin/echo -ne $@")
286 param = r"some text\nanother line\nok, last one\n"
287- expected = ["some text", "another line", "ok, last one"]
288+ expected = [b"some text", b"another line", b"ok, last one"]
289 lines = []
290
291 def line_received(line):
292@@ -87,9 +84,9 @@
293 """
294 If output ends with more than one newline, empty lines are preserved.
295 """
296- create_file(self.command, "#!/bin/sh\n/bin/echo -ne $@")
297+ create_file(self.command, b"#!/bin/sh\n/bin/echo -ne $@")
298 param = r"some text\nanother line\n\n\n"
299- expected = ["some text", "another line", "", ""]
300+ expected = [b"some text", b"another line", b"", b""]
301 lines = []
302
303 def line_received(line):
304@@ -109,9 +106,9 @@
305 If output ends without a newline, the line is still passed to the
306 callback.
307 """
308- create_file(self.command, "#!/bin/sh\n/bin/echo -ne $@")
309+ create_file(self.command, b"#!/bin/sh\n/bin/echo -ne $@")
310 param = r"some text\nanother line\nok, last one"
311- expected = ["some text", "another line", "ok, last one"]
312+ expected = [b"some text", b"another line", b"ok, last one"]
313 lines = []
314
315 def line_received(line):
316@@ -126,16 +123,15 @@
317 result.addCallback(callback)
318 return result
319
320- @skipIf(_PY3, 'Takes long with Python3, probably unclean Reactor')
321 def test_spawn_process_with_stdin(self):
322 """
323 Optionally C{spawn_process} accepts a C{stdin} argument.
324 """
325- create_file(self.command, "#!/bin/sh\n/bin/cat")
326+ create_file(self.command, b"#!/bin/sh\n/bin/cat")
327
328 def callback(args):
329 out, err, code = args
330- self.assertEqual("hello", out)
331+ self.assertEqual(b"hello", out)
332
333 result = spawn_process(self.command, stdin="hello")
334 result.addCallback(callback)
335
336=== modified file 'landscape/lib/twisted_util.py'
337--- landscape/lib/twisted_util.py 2017-01-10 16:12:53 +0000
338+++ landscape/lib/twisted_util.py 2017-03-13 10:45:51 +0000
339@@ -1,10 +1,10 @@
340+import io
341+
342 from twisted.internet.defer import DeferredList, Deferred
343 from twisted.internet.protocol import ProcessProtocol
344 from twisted.internet.process import Process, ProcessReader
345 from twisted.internet import reactor
346-from twisted.python.compat import itervalues
347-
348-from landscape.compat import StringIO
349+from twisted.python.compat import itervalues, networkString
350
351
352 def gather_results(deferreds, consume_errors=False):
353@@ -20,16 +20,16 @@
354
355 def __init__(self, deferred, stdin=None, line_received=None):
356 self.deferred = deferred
357- self.outBuf = StringIO()
358- self.errBuf = StringIO()
359+ self.outBuf = io.BytesIO()
360+ self.errBuf = io.BytesIO()
361 self.errReceived = self.errBuf.write
362 self.stdin = stdin
363 self.line_received = line_received
364- self._partial_line = ""
365+ self._partial_line = b""
366
367 def connectionMade(self):
368 if self.stdin is not None:
369- self.transport.write(self.stdin)
370+ self.transport.write(networkString(self.stdin))
371 self.transport.closeStdin()
372
373 def outReceived(self, data):
374@@ -41,7 +41,7 @@
375 # data may contain more than one line, so we split the output and save
376 # the last line. If it's an empty string nothing happens, otherwise it
377 # will be returned once complete
378- lines = data.split("\n")
379+ lines = data.split(b"\n")
380 lines[0] = self._partial_line + lines[0]
381 self._partial_line = lines.pop()
382
383@@ -51,7 +51,7 @@
384 def processEnded(self, reason):
385 if self._partial_line:
386 self.line_received(self._partial_line)
387- self._partial_line = ""
388+ self._partial_line = b""
389 out = self.outBuf.getvalue()
390 err = self.errBuf.getvalue()
391 e = reason.value
392
393=== modified file 'landscape/lib/vm_info.py'
394--- landscape/lib/vm_info.py 2016-09-23 15:43:36 +0000
395+++ landscape/lib/vm_info.py 2017-03-13 10:45:51 +0000
396@@ -34,7 +34,7 @@
397 for filename in ("container_type", "systemd/container"):
398 path = os.path.join(run_path, filename)
399 if os.path.exists(path):
400- return read_file(path).strip()
401+ return read_file(path, encoding="utf-8").strip()
402 return ""
403
404
405@@ -52,7 +52,7 @@
406
407 def _get_vm_by_vendor(sys_vendor_path):
408 """Return the VM type string (possibly empty) based on the vendor."""
409- vendor = read_file(sys_vendor_path).lower()
410+ vendor = read_file(sys_vendor_path, encoding="utf-8").lower()
411 # Use lower-key string for vendors, since we do case-insentive match.
412 content_vendors_map = (
413 ("bochs", "kvm"),
414@@ -72,7 +72,8 @@
415 def _get_vm_legacy(root_path):
416 """Check if the host is virtualized looking at /proc/cpuinfo content."""
417 try:
418- cpuinfo = read_file(os.path.join(root_path, "proc/cpuinfo"))
419+ cpuinfo = read_file(
420+ os.path.join(root_path, "proc/cpuinfo"), encoding="utf-8")
421 except (IOError, OSError):
422 return ""
423

Subscribers

People subscribed via source and target branches

to all changes: