on master help sign in
Merge pull request #486 from a01200356/master

[enh] WolframAlpha no API engine (and tests for both)

commitb5a3dfca60f23bac10ade068c40729f030bbad63 authorAdam Tauber <asciimoo@gmail.com> dateTue 19 Jan 2016, 17:02 (11 years ago) parent09b767330bfbf2
5 files changed+621−16
searx/engines/wolframalpha_api.py+26−9 searx/engines/wolframalpha_noapi.py+86 searx/settings.yml+9−7 tests/unit/engines/test_wolframalpha_api.py+307 tests/unit/engines/test_wolframalpha_noapi.py+193
searx/engines/wolframalpha_api.py+26−9
@@ -10,11 +10,18 @@
1010
1111from urllib import urlencode
1212from lxml import etree
13+from re import search
1314
1415# search-url
1516base_url = 'http://api.wolframalpha.com/v2/query'
1617search_url = base_url + '?appid={api_key}&{query}&format=plaintext'
17−api_key = ''
18+site_url = 'http://www.wolframalpha.com/input/?{query}'
19+api_key = '' # defined in settings.yml
20+
21+# xpath variables
22+failure_xpath = '/queryresult[attribute::success="false"]'
23+answer_xpath = '//pod[attribute::primary="true"]/subpod/plaintext'
24+input_xpath = '//pod[starts-with(attribute::title, "Input")]/subpod/plaintext'
1825
1926
2027# do search-request
@@ -45,16 +52,26 @@ def response(resp):
4552 search_results = etree.XML(resp.content)
4653
4754 # return empty array if there are no results
48− if search_results.xpath('/queryresult[attribute::success="false"]'):
55+ if search_results.xpath(failure_xpath):
4956 return []
5057
51− # parse result
52− result = search_results.xpath('//pod[attribute::primary="true"]/subpod/plaintext')[0].text
53− result = replace_pua_chars(result)
58+ # parse answers
59+ answers = search_results.xpath(answer_xpath)
60+ if answers:
61+ for answer in answers:
62+ answer = replace_pua_chars(answer.text)
63+
64+ results.append({'answer': answer})
65+
66+ # if there's no input section in search_results, check if answer has the input embedded (before their "=" sign)
67+ try:
68+ query_input = search_results.xpath(input_xpath)[0].text
69+ except IndexError:
70+ query_input = search(u'([^\uf7d9]+)', answers[0].text).group(1)
5471
55− # append result
56− # TODO: shouldn't it bind the source too?
57− results.append({'answer': result})
72+ # append link to site
73+ result_url = site_url.format(query=urlencode({'i': query_input.encode('utf-8')}))
74+ results.append({'url': result_url,
75+ 'title': query_input + " - Wolfram|Alpha"})
5876
59− # return results
6077 return results
searx/engines/wolframalpha_noapi.pynew file+86
@@ -0,0 +1,86 @@
1+# WolframAlpha (Maths)
2+#
3+# @website http://www.wolframalpha.com/
4+# @provide-api yes (http://api.wolframalpha.com/v2/)
5+#
6+# @using-api no
7+# @results HTML
8+# @stable no
9+# @parse answer
10+
11+from re import search, sub
12+from json import loads
13+from urllib import urlencode
14+from lxml import html
15+import HTMLParser
16+
17+# search-url
18+url = 'http://www.wolframalpha.com/'
19+search_url = url + 'input/?{query}'
20+
21+# xpath variables
22+scripts_xpath = '//script'
23+title_xpath = '//title'
24+failure_xpath = '//p[attribute::class="pfail"]'
25+
26+
27+# do search-request
28+def request(query, params):
29+ params['url'] = search_url.format(query=urlencode({'i': query}))
30+
31+ return params
32+
33+
34+# get response from search-request
35+def response(resp):
36+ results = []
37+ line = None
38+
39+ dom = html.fromstring(resp.text)
40+ scripts = dom.xpath(scripts_xpath)
41+
42+ # the answer is inside a js function
43+ # answer can be located in different 'pods', although by default it should be in pod_0200
44+ possible_locations = ['pod_0200\.push\((.*)',
45+ 'pod_0100\.push\((.*)']
46+
47+ # failed result
48+ if dom.xpath(failure_xpath):
49+ return results
50+
51+ # get line that matches the pattern
52+ for pattern in possible_locations:
53+ for script in scripts:
54+ try:
55+ line = search(pattern, script.text_content()).group(1)
56+ break
57+ except AttributeError:
58+ continue
59+ if line:
60+ break
61+
62+ if line:
63+ # extract answer from json
64+ answer = line[line.find('{'):line.rfind('}') + 1]
65+ try:
66+ answer = loads(answer)
67+ except Exception:
68+ answer = loads(answer.encode('unicode-escape'))
69+ answer = answer['stringified']
70+
71+ # clean plaintext answer
72+ h = HTMLParser.HTMLParser()
73+ answer = h.unescape(answer.decode('unicode-escape'))
74+ answer = sub(r'\\', '', answer)
75+
76+ results.append({'answer': answer})
77+
78+ # user input is in first part of title
79+ title = dom.xpath(title_xpath)[0].text.encode('utf-8')
80+ result_url = request(title[:-16], {})['url']
81+
82+ # append result
83+ results.append({'url': result_url,
84+ 'title': title.decode('utf-8')})
85+
86+ return results
searx/settings.yml+9−7
@@ -300,13 +300,15 @@ engines:
300300 engine : vimeo
301301 shortcut : vm
302302
303−# You can use the engine using the official stable API, but you need an API key
304−# See : http://products.wolframalpha.com/api/
305−# - name : wolframalpha
306−# shortcut : wa
307−# engine : wolframalpha_api
308−# api_key: 'apikey' # required!
309−# timeout: 6.0
303+ - name : wolframalpha
304+ shortcut : wa
305+ # You can use the engine using the official stable API, but you need an API key
306+ # See : http://products.wolframalpha.com/api/
307+ # engine : wolframalpha_api
308+ # api_key: 'apikey' # required!
309+ engine : wolframalpha_noapi
310+ timeout: 6.0
311+ disabled : True
310312
311313#The blekko technology and team have joined IBM Watson! -> https://blekko.com/
312314# - name : blekko images
tests/unit/engines/test_wolframalpha_api.pynew file+307
@@ -0,0 +1,307 @@
1+# -*- coding: utf-8 -*-
2+from collections import defaultdict
3+import mock
4+from searx.engines import wolframalpha_api
5+from searx.testing import SearxTestCase
6+
7+
8+class TestWolframAlphaAPIEngine(SearxTestCase):
9+
10+ def test_request(self):
11+ query = 'test_query'
12+ api_key = 'XXXXXX-XXXXXXXXXX'
13+ dicto = defaultdict(dict)
14+ dicto['api_key'] = api_key
15+ params = wolframalpha_api.request(query, dicto)
16+
17+ self.assertIn('url', params)
18+ self.assertIn(query, params['url'])
19+ self.assertIn('wolframalpha.com', params['url'])
20+
21+ self.assertIn('api_key', params)
22+ self.assertIn(api_key, params['api_key'])
23+
24+ def test_response(self):
25+ self.assertRaises(AttributeError, wolframalpha_api.response, None)
26+ self.assertRaises(AttributeError, wolframalpha_api.response, [])
27+ self.assertRaises(AttributeError, wolframalpha_api.response, '')
28+ self.assertRaises(AttributeError, wolframalpha_api.response, '[]')
29+
30+ xml = '''<?xml version='1.0' encoding='UTF-8'?>
31+ <queryresult success='false' error='false' />
32+ '''
33+ # test failure
34+ response = mock.Mock(content=xml)
35+ self.assertEqual(wolframalpha_api.response(response), [])
36+
37+ xml = """<?xml version='1.0' encoding='UTF-8'?>
38+ <queryresult success='true'
39+ error='false'
40+ numpods='6'
41+ datatypes=''
42+ timedout=''
43+ timedoutpods=''
44+ timing='0.684'
45+ parsetiming='0.138'
46+ parsetimedout='false'
47+ recalculate=''
48+ id='MSPa416020a7966dachc463600000f9c66cc21444cfg'
49+ host='http://www3.wolframalpha.com'
50+ server='6'
51+ related='http://www3.wolframalpha.com/api/v2/relatedQueries.jsp?...'
52+ version='2.6'>
53+ <pod title='Input'
54+ scanner='Identity'
55+ id='Input'
56+ position='100'
57+ error='false'
58+ numsubpods='1'>
59+ <subpod title=''>
60+ <plaintext>sqrt(-1)</plaintext>
61+ </subpod>
62+ </pod>
63+ <pod title='Result'
64+ scanner='Simplification'
65+ id='Result'
66+ position='200'
67+ error='false'
68+ numsubpods='1'
69+ primary='true'>
70+ <subpod title=''>
71+ <plaintext></plaintext>
72+ </subpod>
73+ <states count='1'>
74+ <state name='Step-by-step solution'
75+ input='Result__Step-by-step solution' />
76+ </states>
77+ </pod>
78+ <pod title='Polar coordinates'
79+ scanner='Numeric'
80+ id='PolarCoordinates'
81+ position='300'
82+ error='false'
83+ numsubpods='1'>
84+ <subpod title=''>
85+ <plaintext>r1 (radius), θ90° (angle)</plaintext>
86+ </subpod>
87+ </pod>
88+ <pod title='Position in the complex plane'
89+ scanner='Numeric'
90+ id='PositionInTheComplexPlane'
91+ position='400'
92+ error='false'
93+ numsubpods='1'>
94+ <subpod title=''>
95+ <plaintext></plaintext>
96+ </subpod>
97+ </pod>
98+ <pod title='All 2nd roots of -1'
99+ scanner='RootsOfUnity'
100+ id=''
101+ position='500'
102+ error='false'
103+ numsubpods='2'>
104+ <subpod title=''>
105+ <plaintext> (principal root)</plaintext>
106+ </subpod>
107+ <subpod title=''>
108+ <plaintext>-</plaintext>
109+ </subpod>
110+ </pod>
111+ <pod title='Plot of all roots in the complex plane'
112+ scanner='RootsOfUnity'
113+ id='PlotOfAllRootsInTheComplexPlane'
114+ position='600'
115+ error='false'
116+ numsubpods='1'>
117+ <subpod title=''>
118+ <plaintext></plaintext>
119+ </subpod>
120+ </pod>
121+ </queryresult>
122+ """
123+ # test private user area char in response
124+ response = mock.Mock(content=xml)
125+ results = wolframalpha_api.response(response)
126+ self.assertEqual(type(results), list)
127+ self.assertEqual(len(results), 2)
128+ self.assertIn('i', results[0]['answer'])
129+ self.assertIn('sqrt(-1) - Wolfram|Alpha', results[1]['title'])
130+ self.assertEquals('http://www.wolframalpha.com/input/?i=sqrt%28-1%29', results[1]['url'])
131+
132+ xml = """<?xml version='1.0' encoding='UTF-8'?>
133+ <queryresult success='true'
134+ error='false'
135+ numpods='2'
136+ datatypes=''
137+ timedout=''
138+ timedoutpods=''
139+ timing='1.286'
140+ parsetiming='0.255'
141+ parsetimedout='false'
142+ recalculate=''
143+ id='MSPa195222ad740ede5214h30000480ca61h003d3gd6'
144+ host='http://www3.wolframalpha.com'
145+ server='20'
146+ related='http://www3.wolframalpha.com/api/v2/relatedQueries.jsp?id=...'
147+ version='2.6'>
148+ <pod title='Indefinite integral'
149+ scanner='Integral'
150+ id='IndefiniteIntegral'
151+ position='100'
152+ error='false'
153+ numsubpods='1'
154+ primary='true'>
155+ <subpod title=''>
156+ <plaintext>∫1/xxlog(x)+constant</plaintext>
157+ </subpod>
158+ <states count='1'>
159+ <state name='Step-by-step solution'
160+ input='IndefiniteIntegral__Step-by-step solution' />
161+ </states>
162+ <infos count='1'>
163+ <info text='log(x) is the natural logarithm'>
164+ <link url='http://reference.wolfram.com/mathematica/ref/Log.html'
165+ text='Documentation'
166+ title='Mathematica' />
167+ <link url='http://functions.wolfram.com/ElementaryFunctions/Log'
168+ text='Properties'
169+ title='Wolfram Functions Site' />
170+ <link url='http://mathworld.wolfram.com/NaturalLogarithm.html'
171+ text='Definition'
172+ title='MathWorld' />
173+ </info>
174+ </infos>
175+ </pod>
176+ <pod title='Plots of the integral'
177+ scanner='Integral'
178+ id='Plot'
179+ position='200'
180+ error='false'
181+ numsubpods='2'>
182+ <subpod title=''>
183+ <plaintext></plaintext>
184+ <states count='1'>
185+ <statelist count='2'
186+ value='Complex-valued plot'
187+ delimiters=''>
188+ <state name='Complex-valued plot'
189+ input='Plot__1_Complex-valued plot' />
190+ <state name='Real-valued plot'
191+ input='Plot__1_Real-valued plot' />
192+ </statelist>
193+ </states>
194+ </subpod>
195+ <subpod title=''>
196+ <plaintext></plaintext>
197+ <states count='1'>
198+ <statelist count='2'
199+ value='Complex-valued plot'
200+ delimiters=''>
201+ <state name='Complex-valued plot'
202+ input='Plot__2_Complex-valued plot' />
203+ <state name='Real-valued plot'
204+ input='Plot__2_Real-valued plot' />
205+ </statelist>
206+ </states>
207+ </subpod>
208+ </pod>
209+ <assumptions count='1'>
210+ <assumption type='Clash'
211+ word='integral'
212+ template='Assuming &quot;${word}&quot; is ${desc1}. Use as ${desc2} instead'
213+ count='2'>
214+ <value name='IntegralsWord'
215+ desc='an integral'
216+ input='*C.integral-_*IntegralsWord-' />
217+ <value name='MathematicalFunctionIdentityPropertyClass'
218+ desc='a function property'
219+ input='*C.integral-_*MathematicalFunctionIdentityPropertyClass-' />
220+ </assumption>
221+ </assumptions>
222+ </queryresult>
223+ """
224+ # test integral
225+ response = mock.Mock(content=xml)
226+ results = wolframalpha_api.response(response)
227+ self.assertEqual(type(results), list)
228+ self.assertEqual(len(results), 2)
229+ self.assertIn('log(x)+c', results[0]['answer'])
230+ self.assertIn('∫1/xx - Wolfram|Alpha'.decode('utf-8'), results[1]['title'])
231+ self.assertEquals('http://www.wolframalpha.com/input/?i=%E2%88%AB1%2Fx%EF%9D%8Cx', results[1]['url'])
232+
233+ xml = """<?xml version='1.0' encoding='UTF-8'?>
234+ <queryresult success='true'
235+ error='false'
236+ numpods='4'
237+ datatypes='Solve'
238+ timedout=''
239+ timedoutpods=''
240+ timing='0.79'
241+ parsetiming='0.338'
242+ parsetimedout='false'
243+ recalculate=''
244+ id='MSPa7481f7i06d25h3deh2900004810i3a78d9b4fdc'
245+ host='http://www5b.wolframalpha.com'
246+ server='23'
247+ related='http://www5b.wolframalpha.com/api/v2/relatedQueries.jsp?id=...'
248+ version='2.6'>
249+ <pod title='Input interpretation'
250+ scanner='Identity'
251+ id='Input'
252+ position='100'
253+ error='false'
254+ numsubpods='1'>
255+ <subpod title=''>
256+ <plaintext>solve x^2+x0</plaintext>
257+ </subpod>
258+ </pod>
259+ <pod title='Results'
260+ scanner='Solve'
261+ id='Result'
262+ position='200'
263+ error='false'
264+ numsubpods='2'
265+ primary='true'>
266+ <subpod title=''>
267+ <plaintext>x-1</plaintext>
268+ </subpod>
269+ <subpod title=''>
270+ <plaintext>x0</plaintext>
271+ </subpod>
272+ <states count='1'>
273+ <state name='Step-by-step solution'
274+ input='Result__Step-by-step solution' />
275+ </states>
276+ </pod>
277+ <pod title='Root plot'
278+ scanner='Solve'
279+ id='RootPlot'
280+ position='300'
281+ error='false'
282+ numsubpods='1'>
283+ <subpod title=''>
284+ <plaintext></plaintext>
285+ </subpod>
286+ </pod>
287+ <pod title='Number line'
288+ scanner='Solve'
289+ id='NumberLine'
290+ position='400'
291+ error='false'
292+ numsubpods='1'>
293+ <subpod title=''>
294+ <plaintext></plaintext>
295+ </subpod>
296+ </pod>
297+ </queryresult>
298+ """
299+ # test ecuation with multiple answers
300+ response = mock.Mock(content=xml)
301+ results = wolframalpha_api.response(response)
302+ self.assertEqual(type(results), list)
303+ self.assertEqual(len(results), 3)
304+ self.assertIn('x=-1', results[0]['answer'])
305+ self.assertIn('x=0', results[1]['answer'])
306+ self.assertIn('solve x^2+x0 - Wolfram|Alpha'.decode('utf-8'), results[2]['title'])
307+ self.assertEquals('http://www.wolframalpha.com/input/?i=solve+x%5E2%2Bx%EF%9F%990', results[2]['url'])
tests/unit/engines/test_wolframalpha_noapi.pynew file+193
@@ -0,0 +1,193 @@
1+# -*- coding: utf-8 -*-
2+from collections import defaultdict
3+import mock
4+from searx.engines import wolframalpha_noapi
5+from searx.testing import SearxTestCase
6+
7+
8+class TestWolframAlphaNoAPIEngine(SearxTestCase):
9+
10+ def test_request(self):
11+ query = 'test_query'
12+ dicto = defaultdict(dict)
13+ dicto['pageno'] = 1
14+ params = wolframalpha_noapi.request(query, dicto)
15+ self.assertIn('url', params)
16+ self.assertIn(query, params['url'])
17+ self.assertIn('wolframalpha.com', params['url'])
18+
19+ def test_response(self):
20+ self.assertRaises(AttributeError, wolframalpha_noapi.response, None)
21+ self.assertRaises(AttributeError, wolframalpha_noapi.response, [])
22+ self.assertRaises(AttributeError, wolframalpha_noapi.response, '')
23+ self.assertRaises(AttributeError, wolframalpha_noapi.response, '[]')
24+
25+ html = """
26+ <!DOCTYPE html>
27+ <title> Parangaricutirimícuaro - Wolfram|Alpha</title>
28+ <meta charset="utf-8" />
29+ <body>
30+ <div id="closest">
31+ <p class="pfail">Wolfram|Alpha doesn't know how to interpret your input.</p>
32+ <div id="dtips">
33+ <div class="tip">
34+ <span class="tip-title">Tip:&nbsp;</span>
35+ Check your spelling, and use English
36+ <span class="tip-extra"></span>
37+ </div>
38+ </div>
39+ </div>
40+ </body>
41+ </html>
42+ """
43+ # test failed query
44+ response = mock.Mock(text=html)
45+ self.assertEqual(wolframalpha_noapi.response(response), [])
46+
47+ html = """
48+ <!DOCTYPE html>
49+ <title> sqrt(-1) - Wolfram|Alpha</title>
50+ <meta charset="utf-8" />
51+ <body>
52+ <script type="text/javascript">
53+ try {
54+ if (typeof context.jsonArray.popups.pod_0100 == "undefined" ) {
55+ context.jsonArray.popups.pod_0100 = [];
56+ }
57+ context.jsonArray.popups.pod_0100.push( {"stringified": "sqrt(-1)","mInput": "","mOutput": ""});
58+ } catch(e) { }
59+
60+ try {
61+ if (typeof context.jsonArray.popups.pod_0200 == "undefined" ) {
62+ context.jsonArray.popups.pod_0200 = [];
63+ }
64+ context.jsonArray.popups.pod_0200.push( {"stringified": "i","mInput": "","mOutput": ""});
65+ } catch(e) { }
66+ </script>
67+ </body>
68+ </html>
69+ """
70+ # test plaintext
71+ response = mock.Mock(text=html)
72+ results = wolframalpha_noapi.response(response)
73+ self.assertEqual(type(results), list)
74+ self.assertEqual(len(results), 2)
75+ self.assertEquals('i', results[0]['answer'])
76+ self.assertIn('sqrt(-1) - Wolfram|Alpha', results[1]['title'])
77+ self.assertEquals('http://www.wolframalpha.com/input/?i=+sqrt%28-1%29', results[1]['url'])
78+
79+ html = """
80+ <!DOCTYPE html>
81+ <title> integral 1/x - Wolfram|Alpha</title>
82+ <meta charset="utf-8" />
83+ <body>
84+ <script type="text/javascript">
85+ try {
86+ if (typeof context.jsonArray.popups.pod_0100 == "undefined" ) {
87+ context.jsonArray.popups.pod_0100 = [];
88+ }
89+ context.jsonArray.popups.pod_0100.push( {"stringified": "integral 1\/x dx = log(x)+constant"});
90+ } catch(e) { }
91+ </script>
92+ </body>
93+ </html>
94+ """
95+ # test integral
96+ response = mock.Mock(text=html)
97+ results = wolframalpha_noapi.response(response)
98+ self.assertEqual(type(results), list)
99+ self.assertEqual(len(results), 2)
100+ self.assertIn('log(x)+c', results[0]['answer'])
101+ self.assertIn('integral 1/x - Wolfram|Alpha', results[1]['title'])
102+ self.assertEquals('http://www.wolframalpha.com/input/?i=+integral+1%2Fx', results[1]['url'])
103+
104+ html = """
105+ <!DOCTYPE html>
106+ <title> &int;1&#x2f;x &#xf74c;x - Wolfram|Alpha</title>
107+ <meta charset="utf-8" />
108+ <body>
109+ <script type="text/javascript">
110+ try {
111+ if (typeof context.jsonArray.popups.pod_0100 == "undefined" ) {
112+ context.jsonArray.popups.pod_0100 = [];
113+ }
114+ context.jsonArray.popups.pod_0100.push( {"stringified": "integral 1\/x dx = log(x)+constant"});
115+ } catch(e) { }
116+ </script>
117+ </body>
118+ </html>
119+ """
120+ # test input in mathematical notation
121+ response = mock.Mock(text=html)
122+ results = wolframalpha_noapi.response(response)
123+ self.assertEqual(type(results), list)
124+ self.assertEqual(len(results), 2)
125+ self.assertIn('log(x)+c', results[0]['answer'])
126+ self.assertIn('∫1/x x - Wolfram|Alpha'.decode('utf-8'), results[1]['title'])
127+ self.assertEquals('http://www.wolframalpha.com/input/?i=+%E2%88%AB1%2Fx+%EF%9D%8Cx', results[1]['url'])
128+
129+ html = """
130+ <!DOCTYPE html>
131+ <title> 1 euro to yen - Wolfram|Alpha</title>
132+ <meta charset="utf-8" />
133+ <body>
134+ <script type="text/javascript">
135+ try {
136+ if (typeof context.jsonArray.popups.pod_0100 == "undefined" ) {
137+ context.jsonArray.popups.pod_0100 = [];
138+ }
139+ context.jsonArray.popups.pod_0100.push( {"stringified": "convert euro1 (euro) to Japanese yen"});
140+ } catch(e) { }
141+
142+ try {
143+ if (typeof context.jsonArray.popups.pod_0200 == "undefined" ) {
144+ context.jsonArray.popups.pod_0200 = [];
145+ }
146+ context.jsonArray.popups.pod_0200.push( {"stringified": "&yen;130.5 (Japanese yen)"});
147+ } catch(e) { }
148+ </script>
149+ </body>
150+ </html>
151+ """
152+ # test output with htmlentity
153+ response = mock.Mock(text=html)
154+ results = wolframalpha_noapi.response(response)
155+ self.assertEqual(type(results), list)
156+ self.assertEqual(len(results), 2)
157+ self.assertIn('¥'.decode('utf-8'), results[0]['answer'])
158+ self.assertIn('1 euro to yen - Wolfram|Alpha', results[1]['title'])
159+ self.assertEquals('http://www.wolframalpha.com/input/?i=+1+euro+to+yen', results[1]['url'])
160+
161+ html = """
162+ <!DOCTYPE html>
163+ <title> distance from nairobi to kyoto in inches - Wolfram|Alpha</title>
164+ <meta charset="utf-8" />
165+ <body>
166+ <script type="text/javascript">
167+ try {
168+ if (typeof context.jsonArray.popups.pod_0100 == "undefined" ) {
169+ context.jsonArray.popups.pod_0100 = [];
170+ }
171+[...].pod_0100.push( {"stringified": "convert distance | from | Nairobi, Kenya\nto | Kyoto, Japan to inches"});
172+ } catch(e) { }
173+
174+ try {
175+ if (typeof context.jsonArray.popups.pod_0200 == "undefined" ) {
176+ context.jsonArray.popups.pod_0200 = [];
177+ }
178+pod_0200.push({"stringified": "4.295&times;10^8 inches","mOutput": "Quantity[4.295×10^8,&amp;quot;Inches&amp;quot;]"});
179+
180+ } catch(e) { }
181+ </script>
182+ </body>
183+ </html>
184+ """
185+ # test output with utf-8 character
186+ response = mock.Mock(text=html)
187+ results = wolframalpha_noapi.response(response)
188+ self.assertEqual(type(results), list)
189+ self.assertEqual(len(results), 2)
190+ self.assertIn('4.295×10^8 inches'.decode('utf-8'), results[0]['answer'])
191+ self.assertIn('distance from nairobi to kyoto in inches - Wolfram|Alpha', results[1]['title'])
192+ self.assertEquals('http://www.wolframalpha.com/input/?i=+distance+from+nairobi+to+kyoto+in+inches',
193+ results[1]['url'])