##// END OF EJS Templates
Merge pull request #3481 from takluyver/rmagic-nicer-errors...
Brian E. Granger -
r11098:559a339d merge
parent child Browse files
Show More
@@ -1,660 +1,668 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 ======
4 4 Rmagic
5 5 ======
6 6
7 7 Magic command interface for interactive work with R via rpy2
8 8
9 9 Usage
10 10 =====
11 11
12 12 ``%R``
13 13
14 14 {R_DOC}
15 15
16 16 ``%Rpush``
17 17
18 18 {RPUSH_DOC}
19 19
20 20 ``%Rpull``
21 21
22 22 {RPULL_DOC}
23 23
24 24 ``%Rget``
25 25
26 26 {RGET_DOC}
27 27
28 28 """
29 29
30 30 #-----------------------------------------------------------------------------
31 31 # Copyright (C) 2012 The IPython Development Team
32 32 #
33 33 # Distributed under the terms of the BSD License. The full license is in
34 34 # the file COPYING, distributed as part of this software.
35 35 #-----------------------------------------------------------------------------
36 36
37 37 import sys
38 38 import tempfile
39 39 from glob import glob
40 40 from shutil import rmtree
41 41 from getopt import getopt
42 42
43 43 # numpy and rpy2 imports
44 44
45 45 import numpy as np
46 46
47 47 import rpy2.rinterface as ri
48 48 import rpy2.robjects as ro
49 49 try:
50 50 from rpy2.robjects import pandas2ri
51 51 pandas2ri.activate()
52 52 except ImportError:
53 53 pandas2ri = None
54 54 from rpy2.robjects import numpy2ri
55 55 numpy2ri.activate()
56 56
57 57 # IPython imports
58 58
59 59 from IPython.core.displaypub import publish_display_data
60 60 from IPython.core.magic import (Magics, magics_class, cell_magic, line_magic,
61 61 line_cell_magic, needs_local_scope)
62 62 from IPython.testing.skipdoctest import skip_doctest
63 63 from IPython.core.magic_arguments import (
64 64 argument, magic_arguments, parse_argstring
65 65 )
66 66 from IPython.external.simplegeneric import generic
67 67 from IPython.utils.py3compat import str_to_unicode, unicode_to_str, PY3
68 68
69 69 class RInterpreterError(ri.RRuntimeError):
70 70 """An error when running R code in a %%R magic cell."""
71 71 def __init__(self, line, err, stdout):
72 72 self.line = line
73 73 self.err = err.rstrip()
74 74 self.stdout = stdout.rstrip()
75 75
76 76 def __unicode__(self):
77 77 s = 'Failed to parse and evaluate line %r.\nR error message: %r' % \
78 78 (self.line, self.err)
79 79 if self.stdout and (self.stdout != self.err):
80 80 s += '\nR stdout:\n' + self.stdout
81 81 return s
82 82
83 83 if PY3:
84 84 __str__ = __unicode__
85 85 else:
86 86 def __str__(self):
87 87 return unicode_to_str(unicode(self), 'utf-8')
88 88
89 89 def Rconverter(Robj, dataframe=False):
90 90 """
91 91 Convert an object in R's namespace to one suitable
92 92 for ipython's namespace.
93 93
94 94 For a data.frame, it tries to return a structured array.
95 95 It first checks for colnames, then names.
96 96 If all are NULL, it returns np.asarray(Robj), else
97 97 it tries to construct a recarray
98 98
99 99 Parameters
100 100 ----------
101 101
102 102 Robj: an R object returned from rpy2
103 103 """
104 104 is_data_frame = ro.r('is.data.frame')
105 105 colnames = ro.r('colnames')
106 106 rownames = ro.r('rownames') # with pandas, these could be used for the index
107 107 names = ro.r('names')
108 108
109 109 if dataframe:
110 110 as_data_frame = ro.r('as.data.frame')
111 111 cols = colnames(Robj)
112 112 _names = names(Robj)
113 113 if cols != ri.NULL:
114 114 Robj = as_data_frame(Robj)
115 115 names = tuple(np.array(cols))
116 116 elif _names != ri.NULL:
117 117 names = tuple(np.array(_names))
118 118 else: # failed to find names
119 119 return np.asarray(Robj)
120 120 Robj = np.rec.fromarrays(Robj, names = names)
121 121 return np.asarray(Robj)
122 122
123 123 @generic
124 124 def pyconverter(pyobj):
125 125 """Convert Python objects to R objects. Add types using the decorator:
126 126
127 127 @pyconverter.when_type
128 128 """
129 129 return pyobj
130 130
131 131 # The default conversion for lists seems to make them a nested list. That has
132 132 # some advantages, but is rarely convenient, so for interactive use, we convert
133 133 # lists to a numpy array, which becomes an R vector.
134 134 @pyconverter.when_type(list)
135 135 def pyconverter_list(pyobj):
136 136 return np.asarray(pyobj)
137 137
138 138 if pandas2ri is None:
139 139 # pandas2ri was new in rpy2 2.3.3, so for now we'll fallback to pandas'
140 140 # conversion function.
141 141 try:
142 142 from pandas import DataFrame
143 143 from pandas.rpy.common import convert_to_r_dataframe
144 144 @pyconverter.when_type(DataFrame)
145 145 def pyconverter_dataframe(pyobj):
146 146 return convert_to_r_dataframe(pyobj, strings_as_factors=True)
147 147 except ImportError:
148 148 pass
149 149
150 150 @magics_class
151 151 class RMagics(Magics):
152 152 """A set of magics useful for interactive work with R via rpy2.
153 153 """
154 154
155 155 def __init__(self, shell, Rconverter=Rconverter,
156 156 pyconverter=pyconverter,
157 157 cache_display_data=False):
158 158 """
159 159 Parameters
160 160 ----------
161 161
162 162 shell : IPython shell
163 163
164 164 Rconverter : callable
165 165 To be called on values taken from R before putting them in the
166 166 IPython namespace.
167 167
168 168 pyconverter : callable
169 169 To be called on values in ipython namespace before
170 170 assigning to variables in rpy2.
171 171
172 172 cache_display_data : bool
173 173 If True, the published results of the final call to R are
174 174 cached in the variable 'display_cache'.
175 175
176 176 """
177 177 super(RMagics, self).__init__(shell)
178 178 self.cache_display_data = cache_display_data
179 179
180 180 self.r = ro.R()
181 181
182 182 self.Rstdout_cache = []
183 183 self.pyconverter = pyconverter
184 184 self.Rconverter = Rconverter
185 185
186 186 def eval(self, line):
187 187 '''
188 188 Parse and evaluate a line with rpy2.
189 189 Returns the output to R's stdout() connection
190 190 and the value of eval(parse(line)).
191 191 '''
192 192 old_writeconsole = ri.get_writeconsole()
193 193 ri.set_writeconsole(self.write_console)
194 194 try:
195 195 value = ri.baseenv['eval'](ri.parse(line))
196 196 except (ri.RRuntimeError, ValueError) as exception:
197 197 warning_or_other_msg = self.flush() # otherwise next return seems to have copy of error
198 198 raise RInterpreterError(line, str_to_unicode(str(exception)), warning_or_other_msg)
199 199 text_output = self.flush()
200 200 ri.set_writeconsole(old_writeconsole)
201 201 return text_output, value
202 202
203 203 def write_console(self, output):
204 204 '''
205 205 A hook to capture R's stdout in a cache.
206 206 '''
207 207 self.Rstdout_cache.append(output)
208 208
209 209 def flush(self):
210 210 '''
211 211 Flush R's stdout cache to a string, returning the string.
212 212 '''
213 213 value = ''.join([str_to_unicode(s, 'utf-8') for s in self.Rstdout_cache])
214 214 self.Rstdout_cache = []
215 215 return value
216 216
217 217 @skip_doctest
218 218 @needs_local_scope
219 219 @line_magic
220 220 def Rpush(self, line, local_ns=None):
221 221 '''
222 222 A line-level magic for R that pushes
223 223 variables from python to rpy2. The line should be made up
224 224 of whitespace separated variable names in the IPython
225 225 namespace::
226 226
227 227 In [7]: import numpy as np
228 228
229 229 In [8]: X = np.array([4.5,6.3,7.9])
230 230
231 231 In [9]: X.mean()
232 232 Out[9]: 6.2333333333333343
233 233
234 234 In [10]: %Rpush X
235 235
236 236 In [11]: %R mean(X)
237 237 Out[11]: array([ 6.23333333])
238 238
239 239 '''
240 240 if local_ns is None:
241 241 local_ns = {}
242 242
243 243 inputs = line.split(' ')
244 244 for input in inputs:
245 245 try:
246 246 val = local_ns[input]
247 247 except KeyError:
248 248 try:
249 249 val = self.shell.user_ns[input]
250 250 except KeyError:
251 251 # reraise the KeyError as a NameError so that it looks like
252 252 # the standard python behavior when you use an unnamed
253 253 # variable
254 254 raise NameError("name '%s' is not defined" % input)
255 255
256 256 self.r.assign(input, self.pyconverter(val))
257 257
258 258 @skip_doctest
259 259 @magic_arguments()
260 260 @argument(
261 261 '-d', '--as_dataframe', action='store_true',
262 262 default=False,
263 263 help='Convert objects to data.frames before returning to ipython.'
264 264 )
265 265 @argument(
266 266 'outputs',
267 267 nargs='*',
268 268 )
269 269 @line_magic
270 270 def Rpull(self, line):
271 271 '''
272 272 A line-level magic for R that pulls
273 273 variables from python to rpy2::
274 274
275 275 In [18]: _ = %R x = c(3,4,6.7); y = c(4,6,7); z = c('a',3,4)
276 276
277 277 In [19]: %Rpull x y z
278 278
279 279 In [20]: x
280 280 Out[20]: array([ 3. , 4. , 6.7])
281 281
282 282 In [21]: y
283 283 Out[21]: array([ 4., 6., 7.])
284 284
285 285 In [22]: z
286 286 Out[22]:
287 287 array(['a', '3', '4'],
288 288 dtype='|S1')
289 289
290 290
291 291 If --as_dataframe, then each object is returned as a structured array
292 292 after first passed through "as.data.frame" in R before
293 293 being calling self.Rconverter.
294 294 This is useful when a structured array is desired as output, or
295 295 when the object in R has mixed data types.
296 296 See the %%R docstring for more examples.
297 297
298 298 Notes
299 299 -----
300 300
301 301 Beware that R names can have '.' so this is not fool proof.
302 302 To avoid this, don't name your R objects with '.'s...
303 303
304 304 '''
305 305 args = parse_argstring(self.Rpull, line)
306 306 outputs = args.outputs
307 307 for output in outputs:
308 308 self.shell.push({output:self.Rconverter(self.r(output),dataframe=args.as_dataframe)})
309 309
310 310 @skip_doctest
311 311 @magic_arguments()
312 312 @argument(
313 313 '-d', '--as_dataframe', action='store_true',
314 314 default=False,
315 315 help='Convert objects to data.frames before returning to ipython.'
316 316 )
317 317 @argument(
318 318 'output',
319 319 nargs=1,
320 320 type=str,
321 321 )
322 322 @line_magic
323 323 def Rget(self, line):
324 324 '''
325 325 Return an object from rpy2, possibly as a structured array (if possible).
326 326 Similar to Rpull except only one argument is accepted and the value is
327 327 returned rather than pushed to self.shell.user_ns::
328 328
329 329 In [3]: dtype=[('x', '<i4'), ('y', '<f8'), ('z', '|S1')]
330 330
331 331 In [4]: datapy = np.array([(1, 2.9, 'a'), (2, 3.5, 'b'), (3, 2.1, 'c'), (4, 5, 'e')], dtype=dtype)
332 332
333 333 In [5]: %R -i datapy
334 334
335 335 In [6]: %Rget datapy
336 336 Out[6]:
337 337 array([['1', '2', '3', '4'],
338 338 ['2', '3', '2', '5'],
339 339 ['a', 'b', 'c', 'e']],
340 340 dtype='|S1')
341 341
342 342 In [7]: %Rget -d datapy
343 343 Out[7]:
344 344 array([(1, 2.9, 'a'), (2, 3.5, 'b'), (3, 2.1, 'c'), (4, 5.0, 'e')],
345 345 dtype=[('x', '<i4'), ('y', '<f8'), ('z', '|S1')])
346 346
347 347 '''
348 348 args = parse_argstring(self.Rget, line)
349 349 output = args.output
350 350 return self.Rconverter(self.r(output[0]),dataframe=args.as_dataframe)
351 351
352 352
353 353 @skip_doctest
354 354 @magic_arguments()
355 355 @argument(
356 356 '-i', '--input', action='append',
357 357 help='Names of input variable from shell.user_ns to be assigned to R variables of the same names after calling self.pyconverter. Multiple names can be passed separated only by commas with no whitespace.'
358 358 )
359 359 @argument(
360 360 '-o', '--output', action='append',
361 361 help='Names of variables to be pushed from rpy2 to shell.user_ns after executing cell body and applying self.Rconverter. Multiple names can be passed separated only by commas with no whitespace.'
362 362 )
363 363 @argument(
364 364 '-w', '--width', type=int,
365 365 help='Width of png plotting device sent as an argument to *png* in R.'
366 366 )
367 367 @argument(
368 368 '-h', '--height', type=int,
369 369 help='Height of png plotting device sent as an argument to *png* in R.'
370 370 )
371 371
372 372 @argument(
373 373 '-d', '--dataframe', action='append',
374 374 help='Convert these objects to data.frames and return as structured arrays.'
375 375 )
376 376 @argument(
377 377 '-u', '--units', type=unicode, choices=["px", "in", "cm", "mm"],
378 378 help='Units of png plotting device sent as an argument to *png* in R. One of ["px", "in", "cm", "mm"].'
379 379 )
380 380 @argument(
381 381 '-r', '--res', type=int,
382 382 help='Resolution of png plotting device sent as an argument to *png* in R. Defaults to 72 if *units* is one of ["in", "cm", "mm"].'
383 383 )
384 384 @argument(
385 385 '-p', '--pointsize', type=int,
386 386 help='Pointsize of png plotting device sent as an argument to *png* in R.'
387 387 )
388 388 @argument(
389 389 '-b', '--bg',
390 390 help='Background of png plotting device sent as an argument to *png* in R.'
391 391 )
392 392 @argument(
393 393 '-n', '--noreturn',
394 394 help='Force the magic to not return anything.',
395 395 action='store_true',
396 396 default=False
397 397 )
398 398 @argument(
399 399 'code',
400 400 nargs='*',
401 401 )
402 402 @needs_local_scope
403 403 @line_cell_magic
404 404 def R(self, line, cell=None, local_ns=None):
405 405 '''
406 406 Execute code in R, and pull some of the results back into the Python namespace.
407 407
408 408 In line mode, this will evaluate an expression and convert the returned value to a Python object.
409 409 The return value is determined by rpy2's behaviour of returning the result of evaluating the
410 410 final line.
411 411
412 412 Multiple R lines can be executed by joining them with semicolons::
413 413
414 414 In [9]: %R X=c(1,4,5,7); sd(X); mean(X)
415 415 Out[9]: array([ 4.25])
416 416
417 417 As a cell, this will run a block of R code, without bringing anything back by default::
418 418
419 419 In [10]: %%R
420 420 ....: Y = c(2,4,3,9)
421 421 ....: print(summary(lm(Y~X)))
422 422 ....:
423 423
424 424 Call:
425 425 lm(formula = Y ~ X)
426 426
427 427 Residuals:
428 428 1 2 3 4
429 429 0.88 -0.24 -2.28 1.64
430 430
431 431 Coefficients:
432 432 Estimate Std. Error t value Pr(>|t|)
433 433 (Intercept) 0.0800 2.3000 0.035 0.975
434 434 X 1.0400 0.4822 2.157 0.164
435 435
436 436 Residual standard error: 2.088 on 2 degrees of freedom
437 437 Multiple R-squared: 0.6993,Adjusted R-squared: 0.549
438 438 F-statistic: 4.651 on 1 and 2 DF, p-value: 0.1638
439 439
440 440 In the notebook, plots are published as the output of the cell.
441 441
442 442 %R plot(X, Y)
443 443
444 444 will create a scatter plot of X bs Y.
445 445
446 446 If cell is not None and line has some R code, it is prepended to
447 447 the R code in cell.
448 448
449 449 Objects can be passed back and forth between rpy2 and python via the -i -o flags in line::
450 450
451 451 In [14]: Z = np.array([1,4,5,10])
452 452
453 453 In [15]: %R -i Z mean(Z)
454 454 Out[15]: array([ 5.])
455 455
456 456
457 457 In [16]: %R -o W W=Z*mean(Z)
458 458 Out[16]: array([ 5., 20., 25., 50.])
459 459
460 460 In [17]: W
461 461 Out[17]: array([ 5., 20., 25., 50.])
462 462
463 463 The return value is determined by these rules:
464 464
465 465 * If the cell is not None, the magic returns None.
466 466
467 467 * If the cell evaluates as False, the resulting value is returned
468 468 unless the final line prints something to the console, in
469 469 which case None is returned.
470 470
471 471 * If the final line results in a NULL value when evaluated
472 472 by rpy2, then None is returned.
473 473
474 474 * No attempt is made to convert the final value to a structured array.
475 475 Use the --dataframe flag or %Rget to push / return a structured array.
476 476
477 477 * If the -n flag is present, there is no return value.
478 478
479 479 * A trailing ';' will also result in no return value as the last
480 480 value in the line is an empty string.
481 481
482 482 The --dataframe argument will attempt to return structured arrays.
483 483 This is useful for dataframes with
484 484 mixed data types. Note also that for a data.frame,
485 485 if it is returned as an ndarray, it is transposed::
486 486
487 487 In [18]: dtype=[('x', '<i4'), ('y', '<f8'), ('z', '|S1')]
488 488
489 489 In [19]: datapy = np.array([(1, 2.9, 'a'), (2, 3.5, 'b'), (3, 2.1, 'c'), (4, 5, 'e')], dtype=dtype)
490 490
491 491 In [20]: %%R -o datar
492 492 datar = datapy
493 493 ....:
494 494
495 495 In [21]: datar
496 496 Out[21]:
497 497 array([['1', '2', '3', '4'],
498 498 ['2', '3', '2', '5'],
499 499 ['a', 'b', 'c', 'e']],
500 500 dtype='|S1')
501 501
502 502 In [22]: %%R -d datar
503 503 datar = datapy
504 504 ....:
505 505
506 506 In [23]: datar
507 507 Out[23]:
508 508 array([(1, 2.9, 'a'), (2, 3.5, 'b'), (3, 2.1, 'c'), (4, 5.0, 'e')],
509 509 dtype=[('x', '<i4'), ('y', '<f8'), ('z', '|S1')])
510 510
511 511 The --dataframe argument first tries colnames, then names.
512 512 If both are NULL, it returns an ndarray (i.e. unstructured)::
513 513
514 514 In [1]: %R mydata=c(4,6,8.3); NULL
515 515
516 516 In [2]: %R -d mydata
517 517
518 518 In [3]: mydata
519 519 Out[3]: array([ 4. , 6. , 8.3])
520 520
521 521 In [4]: %R names(mydata) = c('a','b','c'); NULL
522 522
523 523 In [5]: %R -d mydata
524 524
525 525 In [6]: mydata
526 526 Out[6]:
527 527 array((4.0, 6.0, 8.3),
528 528 dtype=[('a', '<f8'), ('b', '<f8'), ('c', '<f8')])
529 529
530 530 In [7]: %R -o mydata
531 531
532 532 In [8]: mydata
533 533 Out[8]: array([ 4. , 6. , 8.3])
534 534
535 535 '''
536 536
537 537 args = parse_argstring(self.R, line)
538 538
539 539 # arguments 'code' in line are prepended to
540 540 # the cell lines
541 541
542 542 if cell is None:
543 543 code = ''
544 544 return_output = True
545 545 line_mode = True
546 546 else:
547 547 code = cell
548 548 return_output = False
549 549 line_mode = False
550 550
551 551 code = ' '.join(args.code) + code
552 552
553 553 # if there is no local namespace then default to an empty dict
554 554 if local_ns is None:
555 555 local_ns = {}
556 556
557 557 if args.input:
558 558 for input in ','.join(args.input).split(','):
559 559 try:
560 560 val = local_ns[input]
561 561 except KeyError:
562 562 try:
563 563 val = self.shell.user_ns[input]
564 564 except KeyError:
565 565 raise NameError("name '%s' is not defined" % input)
566 566 self.r.assign(input, self.pyconverter(val))
567 567
568 568 if getattr(args, 'units') is not None:
569 569 if args.units != "px" and getattr(args, 'res') is None:
570 570 args.res = 72
571 571 args.units = '"%s"' % args.units
572 572
573 573 png_argdict = dict([(n, getattr(args, n)) for n in ['units', 'res', 'height', 'width', 'bg', 'pointsize']])
574 574 png_args = ','.join(['%s=%s' % (o,v) for o, v in png_argdict.items() if v is not None])
575 575 # execute the R code in a temporary directory
576 576
577 577 tmpd = tempfile.mkdtemp()
578 578 self.r('png("%s/Rplots%%03d.png",%s)' % (tmpd.replace('\\', '/'), png_args))
579 579
580 580 text_output = ''
581 if line_mode:
582 for line in code.split(';'):
583 text_result, result = self.eval(line)
581 try:
582 if line_mode:
583 for line in code.split(';'):
584 text_result, result = self.eval(line)
585 text_output += text_result
586 if text_result:
587 # the last line printed something to the console so we won't return it
588 return_output = False
589 else:
590 text_result, result = self.eval(code)
584 591 text_output += text_result
585 if text_result:
586 # the last line printed something to the console so we won't return it
587 return_output = False
588 else:
589 text_result, result = self.eval(code)
590 text_output += text_result
592
593 except RInterpreterError as e:
594 print(e.stdout)
595 if not e.stdout.endswith(e.err):
596 print(e.err)
597 rmtree(tmpd)
598 return
591 599
592 600 self.r('dev.off()')
593 601
594 602 # read out all the saved .png files
595 603
596 604 images = [open(imgfile, 'rb').read() for imgfile in glob("%s/Rplots*png" % tmpd)]
597 605
598 606 # now publish the images
599 607 # mimicking IPython/zmq/pylab/backend_inline.py
600 608 fmt = 'png'
601 609 mimetypes = { 'png' : 'image/png', 'svg' : 'image/svg+xml' }
602 610 mime = mimetypes[fmt]
603 611
604 612 # publish the printed R objects, if any
605 613
606 614 display_data = []
607 615 if text_output:
608 616 display_data.append(('RMagic.R', {'text/plain':text_output}))
609 617
610 618 # flush text streams before sending figures, helps a little with output
611 619 for image in images:
612 620 # synchronization in the console (though it's a bandaid, not a real sln)
613 621 sys.stdout.flush(); sys.stderr.flush()
614 622 display_data.append(('RMagic.R', {mime: image}))
615 623
616 624 # kill the temporary directory
617 625 rmtree(tmpd)
618 626
619 627 # try to turn every output into a numpy array
620 628 # this means that output are assumed to be castable
621 629 # as numpy arrays
622 630
623 631 if args.output:
624 632 for output in ','.join(args.output).split(','):
625 633 self.shell.push({output:self.Rconverter(self.r(output), dataframe=False)})
626 634
627 635 if args.dataframe:
628 636 for output in ','.join(args.dataframe).split(','):
629 637 self.shell.push({output:self.Rconverter(self.r(output), dataframe=True)})
630 638
631 639 for tag, disp_d in display_data:
632 640 publish_display_data(tag, disp_d)
633 641
634 642 # this will keep a reference to the display_data
635 643 # which might be useful to other objects who happen to use
636 644 # this method
637 645
638 646 if self.cache_display_data:
639 647 self.display_cache = display_data
640 648
641 649 # if in line mode and return_output, return the result as an ndarray
642 650 if return_output and not args.noreturn:
643 651 if result != ri.NULL:
644 652 return self.Rconverter(result, dataframe=False)
645 653
646 654 __doc__ = __doc__.format(
647 655 R_DOC = ' '*8 + RMagics.R.__doc__,
648 656 RPUSH_DOC = ' '*8 + RMagics.Rpush.__doc__,
649 657 RPULL_DOC = ' '*8 + RMagics.Rpull.__doc__,
650 658 RGET_DOC = ' '*8 + RMagics.Rget.__doc__
651 659 )
652 660
653 661
654 662 def load_ipython_extension(ip):
655 663 """Load the extension in IPython."""
656 664 ip.register_magics(RMagics)
657 665 # Initialising rpy2 interferes with readline. Since, at this point, we've
658 666 # probably just loaded rpy2, we reset the delimiters. See issue gh-2759.
659 667 if ip.has_readline:
660 668 ip.readline.set_completer_delims(ip.readline_delims)
General Comments 0
You need to be logged in to leave comments. Login now