Merge lp:~kissiel/checkbox/jinja-exporter into lp:checkbox

Proposed by Maciej Kisielewski
Status: Merged
Approved by: Sylvain Pineau
Approved revision: no longer in the source branch.
Merged at revision: 3635
Proposed branch: lp:~kissiel/checkbox/jinja-exporter
Merge into: lp:checkbox
Diff against target: 430 lines (+208/-27)
12 files modified
checkbox-ng/checkbox_ng/commands/newcli.py (+2/-4)
checkbox-ng/checkbox_ng/commands/sru.py (+4/-4)
checkbox-touch/py/checkbox_touch.py (+1/-3)
plainbox/plainbox/abc.py (+10/-1)
plainbox/plainbox/impl/commands/inv_run.py (+1/-2)
plainbox/plainbox/impl/commands/inv_session.py (+1/-2)
plainbox/plainbox/impl/exporter/__init__.py (+14/-1)
plainbox/plainbox/impl/exporter/jinja2.py (+84/-0)
plainbox/plainbox/impl/exporter/test_init.py (+13/-6)
plainbox/plainbox/impl/exporter/test_jinja2.py (+75/-0)
plainbox/plainbox/impl/exporter/text.py (+2/-2)
plainbox/plainbox/impl/highlevel.py (+1/-2)
To merge this branch: bzr merge lp:~kissiel/checkbox/jinja-exporter
Reviewer Review Type Date Requested Status
Sylvain Pineau Approve
Review via email: mp+254553@code.launchpad.net

Description of the change

This MR brings basic, generic jinja2 exporter. To create concrete exporter instantiate Jinja2SessionStateExporter with particular jinja template.

9f799ce plainbox:exporter: add jinja2 exporter
a3b690d plainbox:abc: add dump_from_session_manager method to exporter iface
33151eb plainbox:abc: remove get_session_data_subset from ISessionStateExporter iface

To post a comment you must log in.
Revision history for this message
Zygmunt Krynicki (zyga) wrote :
Download full text (3.2 KiB)

12:11 <@zyga> kissiel: -1 on your merge request, don't change the ABC
interface on the first patch
12:11 <@zyga> kissiel: that's broken broken broken, stuff that uses it
as a mock spec will fail, and it's a bad idea in general
12:12 < kissiel> mm-kay
12:12 <@zyga> kissiel: even if it works I think it should be there
until we get rid of it
12:12 <@zyga> kissiel: from the core classes
12:12 <@zyga> kissiel: typically abc interfaces are the last to go
12:12 <@zyga> kissiel: +1 on the next patch but again, wrong ordering
12:13 <@zyga> kissiel: first add it to the places that want it
12:13 <@zyga> kissiel: then declare it as interface
12:13 <@zyga> kissiel: otherwise between those commits, stuff is broken
12:13 <@zyga> kissiel: what I want to see
12:13 <@zyga> kissiel: add the new method to the base exporter class
12:13 <@zyga> kissiel: switch all tools to use the new method
12:13 <@zyga> kissiel: @deprecate the old method
12:13 <@zyga> kissiel: add the interface
12:13 <@zyga> kissiel: see if per commit stuff works
12:14 <@zyga> kissiel: add jinja stuff
12:14 <@zyga> kissiel: logical progression
12:14 <@zyga> kissiel: in the jinja code, use new-form super(), we're
not likely going to support 2.7 so no need to be verbose
12:15 <@zyga> kissiel: document the template argument, is that a
string or a filename?
12:15 <@zyga> kissiel: and one more thing, it's broken today so it's
not your fault at all but document how the stream is supposed to be
set up (binary or text)
12:15 <@zyga> kissiel: it's essential to know and we should stick to it
12:15 < kissiel> zyga, one thing tho, 'switch all the tools' this is
long-ish, and it's not like this can be easily fixed
12:16 <@zyga> kissiel: when mocking use spec_set= please, on the
interface if one exists, on the implementation if there is none
12:16 < kissiel> zyga, instead of patching it it should be replaced,
so maybe deprecate it and move to new exporters?
12:16 <@zyga> kissiel: if this cannot be easily fixed then don't do
the new method at all
12:16 <@zyga> kissiel: I mean currently all the expoerer code does
subset -> dump
12:17 <@zyga> kissiel: if you change that to dump_session() then this is fine
12:17 <@zyga> kissiel: if not then this is pointless, we want to have
one code that works in all the apps, not two that work in some and not
all, (or am I missing something?)
12:17 <@zyga> kissiel: you can implement the new interface on the
current classes easily from what I see
12:18 < kissiel> zyga, if every client class uses -> get_subset() &&
dump() then yes
12:18 <@zyga> kissiel: but from what I see, you're not implementing
the subset interface on Jinjj2SSE so you cannot just start using it
now
12:18 <@zyga> kissiel: that's what they have to use as that's the only
common interface
12:18 <@zyga> kissiel: all exporters are the same, options, subset, dump

On Mon, Mar 30, 2015 at 12:17 PM, Maciej Kisielewski
<email address hidden> wrote:
> The proposal to merge lp:~kissiel/checkbox/jinja-exporter into lp:checkbox has been updated.
>
> Status: Needs review => Work in progress
>
> For more details, see:
> https://code.launchpad.net/~kissiel/checkbox/jinja-exporter/+merge/254553
> --
> You are su...

Read more...

Revision history for this message
Zygmunt Krynicki (zyga) wrote :

Just one comment about options and I'll be happy.

lp:~kissiel/checkbox/jinja-exporter updated
3634. By Zygmunt Krynicki

"automatic merge of lp:~zyga/checkbox/fix-1438643/ by tarmac [r=sylvain-pineau][bug=1438643][author=zyga]"

Revision history for this message
Sylvain Pineau (sylvain-pineau) wrote :

+1, We'll use this base exporter A LOT now. thanks

review: Approve
Revision history for this message
Zygmunt Krynicki (zyga) wrote :

One more mistake, sorry

lp:~kissiel/checkbox/jinja-exporter updated
3635. By Maciej Kisielewski

"automatic merge of lp:~kissiel/checkbox/jinja-exporter/ by tarmac [r=sylvain-pineau][bug=][author=kissiel]"

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'checkbox-ng/checkbox_ng/commands/newcli.py'
2--- checkbox-ng/checkbox_ng/commands/newcli.py 2015-03-05 16:20:40 +0000
3+++ checkbox-ng/checkbox_ng/commands/newcli.py 2015-03-31 13:57:16 +0000
4@@ -333,8 +333,7 @@
5 print(self.C.header(_("Results")))
6 exporter = get_all_exporters()['text']()
7 exported_stream = io.BytesIO()
8- data_subset = exporter.get_session_data_subset(self.manager.state)
9- exporter.dump(data_subset, exported_stream)
10+ exporter.dump_from_session_manager(self.manager, exported_stream)
11 exported_stream.seek(0) # Need to rewind the file, puagh
12 # This requires a bit more finesse, as exporters output bytes
13 # and stdout needs a string.
14@@ -363,7 +362,6 @@
15 actual_options = [opt for opt in exp_options
16 if opt in exporter_cls.supported_option_list]
17 exporter = exporter_cls(actual_options)
18- data_subset = exporter.get_session_data_subset(self.manager.state)
19 results_path = results_file
20 if exporter_cls is XMLSessionStateExporter:
21 results_path = submission_file
22@@ -372,7 +370,7 @@
23 if exporter_cls is XLSXSessionStateExporter:
24 results_path = results_path.replace('html', 'xlsx')
25 with open(results_path, "wb") as stream:
26- exporter.dump(data_subset, stream)
27+ exporter.dump_from_session_manager(self.manager, stream)
28 print()
29 print(_("Saving submission file to {}").format(submission_file))
30 self.submission_file = submission_file
31
32=== modified file 'checkbox-ng/checkbox_ng/commands/sru.py'
33--- checkbox-ng/checkbox_ng/commands/sru.py 2014-11-24 10:57:17 +0000
34+++ checkbox-ng/checkbox_ng/commands/sru.py 2015-03-31 13:57:16 +0000
35@@ -116,10 +116,10 @@
36
37 def _save_results(self):
38 print(_("Saving results to {0}").format(self.config.fallback_file))
39- data = self.exporter.get_session_data_subset(self.session)
40 with open(self.config.fallback_file, "wt", encoding="UTF-8") as stream:
41 translating_stream = ByteStringStreamTranslator(stream, "UTF-8")
42- self.exporter.dump(data, translating_stream)
43+ self.exporter.dump_from_session_manager(self.session.manager,
44+ translating_stream)
45
46 def _submit_results(self):
47 print(_("Submitting results to {0} for secure_id {1}").format(
48@@ -128,10 +128,10 @@
49 # Create the transport object
50 transport = CertificationTransport(self.config.c3_url, options_string)
51 # Prepare the data for submission
52- data = self.exporter.get_session_data_subset(self.session)
53 with tempfile.NamedTemporaryFile(mode='w+b') as stream:
54 # Dump the data to the temporary file
55- self.exporter.dump(data, stream)
56+ self.exporter.dump_from_session_manager(self.session.manager,
57+ stream)
58 # Flush and rewind
59 stream.flush()
60 stream.seek(0)
61
62=== modified file 'checkbox-touch/py/checkbox_touch.py'
63--- checkbox-touch/py/checkbox_touch.py 2015-03-20 13:15:08 +0000
64+++ checkbox-touch/py/checkbox_touch.py 2015-03-31 13:57:16 +0000
65@@ -728,9 +728,7 @@
66 stream):
67 exporter_cls = get_all_exporters()[output_format]
68 exporter = exporter_cls(option_list)
69- data_subset = exporter.get_session_data_subset(
70- self.context.state)
71- exporter.dump(data_subset, stream)
72+ export.dump_from_session_manager(self.manager, stream)
73
74 def _checkpoint(self):
75 self.context.state.metadata.app_blob = self._get_app_blob()
76
77=== modified file 'plainbox/plainbox/abc.py'
78--- plainbox/plainbox/abc.py 2015-02-09 09:08:25 +0000
79+++ plainbox/plainbox/abc.py 2015-03-31 13:57:16 +0000
80@@ -1009,7 +1009,7 @@
81 """
82
83 @abstractmethod
84- def get_session_data_subset(self, session):
85+ def get_session_data_subset(self, session_manager):
86 """
87 Compute a subset of session data.
88
89@@ -1038,6 +1038,15 @@
90 """
91 # TODO: Add a way for the stream to be binary as well.
92
93+ @abstractmethod
94+ def dump_from_session_manager(self, session_manager, stream):
95+ """
96+ Dump session information pulled from session manager to stream.
97+
98+ This method takes session manager instance, extracts session
99+ information from it, and dumps it to a stream.
100+ """
101+
102
103 class ISessionStateTransport(metaclass=ABCMeta):
104 """
105
106=== modified file 'plainbox/plainbox/impl/commands/inv_run.py'
107--- plainbox/plainbox/impl/commands/inv_run.py 2015-02-25 23:54:36 +0000
108+++ plainbox/plainbox/impl/commands/inv_run.py 2015-03-31 13:57:16 +0000
109@@ -808,8 +808,7 @@
110 def export_and_send_results(self):
111 # Get a stream with exported session data.
112 exported_stream = io.BytesIO()
113- data_subset = self.exporter.get_session_data_subset(self.state)
114- self.exporter.dump(data_subset, exported_stream)
115+ self.exporter.dump_from_session_manager(self.manager, exported_stream)
116 exported_stream.seek(0) # Need to rewind the file, puagh
117 # Write the stream to file if requested
118 self._save_results(self.ns.output_file, exported_stream)
119
120=== modified file 'plainbox/plainbox/impl/commands/inv_session.py'
121--- plainbox/plainbox/impl/commands/inv_session.py 2015-02-06 15:51:40 +0000
122+++ plainbox/plainbox/impl/commands/inv_session.py 2015-03-31 13:57:16 +0000
123@@ -156,8 +156,7 @@
124 self._get_all_units(), storage, flags=self.ns.flag)
125 # Get a stream with exported session data.
126 exported_stream = io.BytesIO()
127- data_subset = exporter.get_session_data_subset(manager.state)
128- exporter.dump(data_subset, exported_stream)
129+ exporter.dump_from_session_manager(manager, exported_stream)
130 exported_stream.seek(0) # Need to rewind the file, puagh
131 # Write the stream to file if requested
132 if self.ns.output_file is sys.stdout:
133
134=== modified file 'plainbox/plainbox/impl/exporter/__init__.py'
135--- plainbox/plainbox/impl/exporter/__init__.py 2015-02-13 14:58:46 +0000
136+++ plainbox/plainbox/impl/exporter/__init__.py 2015-03-31 13:57:16 +0000
137@@ -35,6 +35,7 @@
138
139 from plainbox.i18n import gettext as _
140 from plainbox.abc import ISessionStateExporter
141+from plainbox.impl import deprecated
142
143 logger = getLogger("plainbox.exporter")
144
145@@ -161,7 +162,18 @@
146 """
147 return cls.SUPPORTED_OPTION_LIST
148
149- def get_session_data_subset(self, session):
150+ def dump_from_session_manager(self, session_manager, stream):
151+ """
152+ Dump session information pulled from session manager to stream.
153+
154+ This method takes session manager instance, extracts session
155+ information from it, and dumps it to a stream.
156+ """
157+
158+ self.dump(self.get_session_data_subset(session_manager), stream)
159+
160+ @deprecated('0.21', 'use .dump_from_session_manager instead')
161+ def get_session_data_subset(self, session_manager):
162 """
163 Compute a subset of session data.
164
165@@ -175,6 +187,7 @@
166 data = {
167 'result_map': {}
168 }
169+ session = session_manager.state
170 if self.OPTION_WITH_JOB_LIST in self._option_list:
171 data['job_list'] = [job.id for job in session.job_list]
172 if self.OPTION_WITH_RUN_LIST in self._option_list:
173
174=== added file 'plainbox/plainbox/impl/exporter/jinja2.py'
175--- plainbox/plainbox/impl/exporter/jinja2.py 1970-01-01 00:00:00 +0000
176+++ plainbox/plainbox/impl/exporter/jinja2.py 2015-03-31 13:57:16 +0000
177@@ -0,0 +1,84 @@
178+# This file is part of Checkbox.
179+#
180+# Copyright 2015 Canonical Ltd.
181+# Written by:
182+# Maciej Kisielewski <maciej.kisielewski@canonical.com>
183+#
184+# Checkbox is free software: you can redistribute it and/or modify
185+# it under the terms of the GNU General Public License version 3,
186+# as published by the Free Software Foundation.
187+#
188+# Checkbox is distributed in the hope that it will be useful,
189+# but WITHOUT ANY WARRANTY; without even the implied warranty of
190+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
191+# GNU General Public License for more details.
192+#
193+# You should have received a copy of the GNU General Public License
194+# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
195+
196+"""
197+:mod:`plainbox.impl.exporter.jinja2` -- exporter using jinja2 templates
198+=======================================================================
199+
200+.. warning::
201+ THIS MODULE DOES NOT HAVE A STABLE PUBLIC API
202+"""
203+
204+from jinja2 import Template
205+
206+from plainbox.abc import ISessionStateExporter
207+
208+
209+class Jinja2SessionStateExporter(ISessionStateExporter):
210+
211+ """Session state exporter that renders output using jinja2 template."""
212+
213+ def __init__(self, option_list=None, jinja2_template=""):
214+ """
215+ Initialize a new Jinja2SessionStateExporter with given arguments.
216+
217+ :param option_list:
218+ List of options that template might use to fine-tune rendering.
219+ :param jinja2_template:
220+ String with Jinja2 template that will be used.
221+
222+ """
223+ self.option_list = option_list
224+ self.template = Template(jinja2_template)
225+
226+ def dump(self, data, stream):
227+ """
228+ Render report using jinja2 and dump it to stream.
229+
230+ :param data:
231+ Dict to be used when rendering template instance
232+ :param stream:
233+ Byte stream to write to.
234+
235+ """
236+ stream.write(self.template.render(data).encode('UTF-8'))
237+
238+ def dump_from_session_manager(self, session_manager, stream):
239+ """
240+ Extract data from session_manager and dump it into the stream.
241+
242+ :param session_manager:
243+ SessionManager instance that manages session to be exported by
244+ this exporter
245+ :param stream:
246+ Byte stream to write to.
247+
248+ """
249+ data = {
250+ 'manager': session_manager,
251+ 'options': self.option_list,
252+ }
253+ self.dump(data, stream)
254+
255+ def get_session_data_subset(self, session_manager):
256+ """Compute a subset of session data."""
257+ return {'manager': session_manager}
258+
259+ def supported_option_list(cls):
260+ """ Return list of supported exporter options."""
261+ return []
262
263=== modified file 'plainbox/plainbox/impl/exporter/test_init.py'
264--- plainbox/plainbox/impl/exporter/test_init.py 2015-02-13 14:58:46 +0000
265+++ plainbox/plainbox/impl/exporter/test_init.py 2015-03-31 13:57:16 +0000
266@@ -37,8 +37,10 @@
267 from plainbox.impl.job import JobDefinition
268 from plainbox.impl.result import MemoryJobResult, IOLogRecord
269 from plainbox.impl.session import SessionState
270+from plainbox.impl.session.manager import SessionManager
271 from plainbox.impl.testing_utils import make_job, make_job_result
272 from plainbox.impl.unit.category import CategoryUnit
273+from plainbox.vendor import mock
274
275
276 class ClassPropertyTests(TestCase):
277@@ -129,8 +131,9 @@
278 def test_defaults(self):
279 # Test all defaults, with all options unset
280 exporter = self.TestSessionStateExporter()
281- session = self.make_test_session()
282- data = exporter.get_session_data_subset(session)
283+ session_manager = mock.Mock(spec_set=SessionManager,
284+ state=self.make_test_session())
285+ data = exporter.get_session_data_subset(session_manager)
286 expected_data = {
287 'result_map': {
288 'job_a': OrderedDict([
289@@ -193,8 +196,10 @@
290 with TemporaryDirectory() as scratch_dir:
291 exporter = self.TestSessionStateExporter(
292 self.TestSessionStateExporter.supported_option_list)
293- session = self.make_realistic_test_session(scratch_dir)
294- data = exporter.get_session_data_subset(session)
295+ session_manager = mock.Mock(
296+ spec_set=SessionManager,
297+ state=self.make_realistic_test_session(scratch_dir))
298+ data = exporter.get_session_data_subset(session_manager)
299 expected_data = {
300 'job_list': ['job_a', 'job_b'],
301 'run_list': ['job_b', 'job_a'],
302@@ -295,7 +300,8 @@
303 })
304 # Create and export a session with the three units
305 state = SessionState([cat_foo, cat_bar, job_froz])
306- data = exporter.get_session_data_subset(state)
307+ session_manager = mock.Mock(spec_set=SessionManager, state=state)
308+ data = exporter.get_session_data_subset(session_manager)
309 # Ensure that only the foo category was used, and the bar category was
310 # discarded as nothing was referencing it
311 self.assertEqual(data['category_map'], {
312@@ -317,7 +323,8 @@
313 })
314 # Create and export a session with that one job
315 state = SessionState([job])
316- data = exporter.get_session_data_subset(state)
317+ session_manager = mock.Mock(spec_set=SessionManager, state=state)
318+ data = exporter.get_session_data_subset(session_manager)
319 # Ensure that the special 'uncategorized' category is used
320 self.assertEqual(data['category_map'], {
321 '2013.com.canonical.plainbox::uncategorised': 'Uncategorised',
322
323=== added file 'plainbox/plainbox/impl/exporter/test_jinja2.py'
324--- plainbox/plainbox/impl/exporter/test_jinja2.py 1970-01-01 00:00:00 +0000
325+++ plainbox/plainbox/impl/exporter/test_jinja2.py 2015-03-31 13:57:16 +0000
326@@ -0,0 +1,75 @@
327+# This file is part of Checkbox.
328+#
329+# Copyright 2015 Canonical Ltd.
330+# Written by:
331+# Maciej Kisielewski <maciej.kisielewski@canonical.com>
332+#
333+# Checkbox is free software: you can redistribute it and/or modify
334+# it under the terms of the GNU General Public License version 3,
335+# as published by the Free Software Foundation.
336+#
337+# Checkbox is distributed in the hope that it will be useful,
338+# but WITHOUT ANY WARRANTY; without even the implied warranty of
339+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
340+# GNU General Public License for more details.
341+#
342+# You should have received a copy of the GNU General Public License
343+# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
344+
345+"""
346+plainbox.impl.exporter.test_jinja2
347+==================================
348+
349+Test definitions for plainbox.impl.exporter.jinja2 module
350+"""
351+
352+from io import BytesIO
353+from textwrap import dedent
354+from unittest import TestCase
355+
356+from plainbox.impl.exporter.jinja2 import Jinja2SessionStateExporter
357+from plainbox.impl.result import MemoryJobResult
358+from plainbox.impl.unit.job import JobDefinition
359+from plainbox.vendor import mock
360+
361+
362+class Jinja2SessionStateExporterTests(TestCase):
363+ def setUp(self):
364+ self.prepare_manager_single_job()
365+
366+ def prepare_manager_single_job(self):
367+ result = mock.Mock(spec_set=MemoryJobResult, outcome='fail',
368+ is_hollow=False)
369+ result.tr_outcome.return_value = 'fail'
370+ job = mock.Mock(spec_set=JobDefinition, id='job_id')
371+ job.tr_summary.return_value = 'job name'
372+ self.manager_single_job = mock.Mock(state=mock.Mock(
373+ run_list=[job],
374+ job_state_map={
375+ job.id: mock.Mock(result=result, job=job)
376+ })
377+ )
378+
379+ def test_plaintext_template(self):
380+ tmpl = dedent(
381+ "{% for job in manager.state.job_state_map %}"
382+ "{{'{:^15}: {}'.format("
383+ "manager.state.job_state_map[job].result.tr_outcome(),"
384+ "manager.state.job_state_map[job].job.tr_summary()) }}\n"
385+ "{% endfor %}")
386+
387+ exporter = Jinja2SessionStateExporter(jinja2_template=tmpl)
388+ stream = BytesIO()
389+ exporter.dump_from_session_manager(self.manager_single_job, stream)
390+ expected_bytes = ' fail : job name\n'.encode('UTF-8')
391+ self.assertEqual(stream.getvalue(), expected_bytes)
392+
393+ def test_empty_template(self):
394+ """
395+ Ensure that exporter doesn't print anything when jinja2 template is
396+ explictly empty.
397+ """
398+ exporter = Jinja2SessionStateExporter(jinja2_template="")
399+ stream = BytesIO()
400+ exporter.dump_from_session_manager(self.manager_single_job, stream)
401+ self.assertEqual(stream.getvalue(), b'')
402
403=== modified file 'plainbox/plainbox/impl/exporter/text.py'
404--- plainbox/plainbox/impl/exporter/text.py 2015-01-08 12:39:39 +0000
405+++ plainbox/plainbox/impl/exporter/text.py 2015-03-31 13:57:16 +0000
406@@ -39,8 +39,8 @@
407 super().__init__(option_list)
408 self.C = Colorizer(color)
409
410- def get_session_data_subset(self, session):
411- return session
412+ def get_session_data_subset(self, session_manager):
413+ return session_manager.state
414
415 def dump(self, session, stream):
416 for job in session.run_list:
417
418=== modified file 'plainbox/plainbox/impl/highlevel.py'
419--- plainbox/plainbox/impl/highlevel.py 2015-02-06 15:51:40 +0000
420+++ plainbox/plainbox/impl/highlevel.py 2015-03-31 13:57:16 +0000
421@@ -365,8 +365,7 @@
422 stream):
423 exporter_cls = get_all_exporters()[output_format]
424 exporter = exporter_cls(option_list)
425- data_subset = exporter.get_session_data_subset(session)
426- exporter.dump(data_subset, stream)
427+ exporter.dump_from_session_manager(session.manager, stream)
428
429 def get_all_transports(self):
430 return [transport for transport in get_all_transports()]

Subscribers

People subscribed via source and target branches