##// END OF EJS Templates
Fix bug where tab-completion with very long filenames would crash the qt console....
Fernando Perez -
Show More
@@ -1,30 +1,44 b''
1 1 # encoding: utf-8
2 2 """Tests for IPython.utils.text"""
3 3
4 4 #-----------------------------------------------------------------------------
5 5 # Copyright (C) 2011 The IPython Development Team
6 6 #
7 7 # Distributed under the terms of the BSD License. The full license is in
8 8 # the file COPYING, distributed as part of this software.
9 9 #-----------------------------------------------------------------------------
10 10
11 11 #-----------------------------------------------------------------------------
12 12 # Imports
13 13 #-----------------------------------------------------------------------------
14 14
15 15 import os
16 16
17 17 import nose.tools as nt
18 18
19 19 from nose import with_setup
20 20
21 21 from IPython.testing import decorators as dec
22 22 from IPython.utils import text
23 23
24 24 #-----------------------------------------------------------------------------
25 25 # Globals
26 26 #-----------------------------------------------------------------------------
27 27
28 28 def test_columnize():
29 """Test columnize with very long inputs"""
30 text.columnize(['a'*180, 'b'*180])
29 """Basic columnize tests."""
30 size = 5
31 items = [l*size for l in 'abc']
32 out = text.columnize(items, displaywidth=80)
33 nt.assert_equals(out, 'aaaaa bbbbb ccccc\n')
34 out = text.columnize(items, displaywidth=10)
35 nt.assert_equals(out, 'aaaaa ccccc\nbbbbb\n')
36
37
38 def test_columnize_long():
39 """Test columnize with inputs longer than the display window"""
40 text.columnize(['a'*81, 'b'*81], displaywidth=80)
41 size = 11
42 items = [l*size for l in 'abc']
43 out = text.columnize(items, displaywidth=size-1)
44 nt.assert_equals(out, '\n'.join(items+['']))
@@ -1,679 +1,686 b''
1 1 # encoding: utf-8
2 2 """
3 3 Utilities for working with strings and text.
4 4 """
5 5
6 6 #-----------------------------------------------------------------------------
7 7 # Copyright (C) 2008-2009 The IPython Development Team
8 8 #
9 9 # Distributed under the terms of the BSD License. The full license is in
10 10 # the file COPYING, distributed as part of this software.
11 11 #-----------------------------------------------------------------------------
12 12
13 13 #-----------------------------------------------------------------------------
14 14 # Imports
15 15 #-----------------------------------------------------------------------------
16 16
17 17 import __main__
18 18
19 19 import os
20 20 import re
21 21 import shutil
22 22 import textwrap
23 23 from string import Formatter
24 24
25 25 from IPython.external.path import path
26 26
27 27 from IPython.utils.io import nlprint
28 28 from IPython.utils.data import flatten
29 29
30 30 #-----------------------------------------------------------------------------
31 31 # Code
32 32 #-----------------------------------------------------------------------------
33 33
34 34
35 35 def unquote_ends(istr):
36 36 """Remove a single pair of quotes from the endpoints of a string."""
37 37
38 38 if not istr:
39 39 return istr
40 40 if (istr[0]=="'" and istr[-1]=="'") or \
41 41 (istr[0]=='"' and istr[-1]=='"'):
42 42 return istr[1:-1]
43 43 else:
44 44 return istr
45 45
46 46
47 47 class LSString(str):
48 48 """String derivative with a special access attributes.
49 49
50 50 These are normal strings, but with the special attributes:
51 51
52 52 .l (or .list) : value as list (split on newlines).
53 53 .n (or .nlstr): original value (the string itself).
54 54 .s (or .spstr): value as whitespace-separated string.
55 55 .p (or .paths): list of path objects
56 56
57 57 Any values which require transformations are computed only once and
58 58 cached.
59 59
60 60 Such strings are very useful to efficiently interact with the shell, which
61 61 typically only understands whitespace-separated options for commands."""
62 62
63 63 def get_list(self):
64 64 try:
65 65 return self.__list
66 66 except AttributeError:
67 67 self.__list = self.split('\n')
68 68 return self.__list
69 69
70 70 l = list = property(get_list)
71 71
72 72 def get_spstr(self):
73 73 try:
74 74 return self.__spstr
75 75 except AttributeError:
76 76 self.__spstr = self.replace('\n',' ')
77 77 return self.__spstr
78 78
79 79 s = spstr = property(get_spstr)
80 80
81 81 def get_nlstr(self):
82 82 return self
83 83
84 84 n = nlstr = property(get_nlstr)
85 85
86 86 def get_paths(self):
87 87 try:
88 88 return self.__paths
89 89 except AttributeError:
90 90 self.__paths = [path(p) for p in self.split('\n') if os.path.exists(p)]
91 91 return self.__paths
92 92
93 93 p = paths = property(get_paths)
94 94
95 95 # FIXME: We need to reimplement type specific displayhook and then add this
96 96 # back as a custom printer. This should also be moved outside utils into the
97 97 # core.
98 98
99 99 # def print_lsstring(arg):
100 100 # """ Prettier (non-repr-like) and more informative printer for LSString """
101 101 # print "LSString (.p, .n, .l, .s available). Value:"
102 102 # print arg
103 103 #
104 104 #
105 105 # print_lsstring = result_display.when_type(LSString)(print_lsstring)
106 106
107 107
108 108 class SList(list):
109 109 """List derivative with a special access attributes.
110 110
111 111 These are normal lists, but with the special attributes:
112 112
113 113 .l (or .list) : value as list (the list itself).
114 114 .n (or .nlstr): value as a string, joined on newlines.
115 115 .s (or .spstr): value as a string, joined on spaces.
116 116 .p (or .paths): list of path objects
117 117
118 118 Any values which require transformations are computed only once and
119 119 cached."""
120 120
121 121 def get_list(self):
122 122 return self
123 123
124 124 l = list = property(get_list)
125 125
126 126 def get_spstr(self):
127 127 try:
128 128 return self.__spstr
129 129 except AttributeError:
130 130 self.__spstr = ' '.join(self)
131 131 return self.__spstr
132 132
133 133 s = spstr = property(get_spstr)
134 134
135 135 def get_nlstr(self):
136 136 try:
137 137 return self.__nlstr
138 138 except AttributeError:
139 139 self.__nlstr = '\n'.join(self)
140 140 return self.__nlstr
141 141
142 142 n = nlstr = property(get_nlstr)
143 143
144 144 def get_paths(self):
145 145 try:
146 146 return self.__paths
147 147 except AttributeError:
148 148 self.__paths = [path(p) for p in self if os.path.exists(p)]
149 149 return self.__paths
150 150
151 151 p = paths = property(get_paths)
152 152
153 153 def grep(self, pattern, prune = False, field = None):
154 154 """ Return all strings matching 'pattern' (a regex or callable)
155 155
156 156 This is case-insensitive. If prune is true, return all items
157 157 NOT matching the pattern.
158 158
159 159 If field is specified, the match must occur in the specified
160 160 whitespace-separated field.
161 161
162 162 Examples::
163 163
164 164 a.grep( lambda x: x.startswith('C') )
165 165 a.grep('Cha.*log', prune=1)
166 166 a.grep('chm', field=-1)
167 167 """
168 168
169 169 def match_target(s):
170 170 if field is None:
171 171 return s
172 172 parts = s.split()
173 173 try:
174 174 tgt = parts[field]
175 175 return tgt
176 176 except IndexError:
177 177 return ""
178 178
179 179 if isinstance(pattern, basestring):
180 180 pred = lambda x : re.search(pattern, x, re.IGNORECASE)
181 181 else:
182 182 pred = pattern
183 183 if not prune:
184 184 return SList([el for el in self if pred(match_target(el))])
185 185 else:
186 186 return SList([el for el in self if not pred(match_target(el))])
187 187
188 188 def fields(self, *fields):
189 189 """ Collect whitespace-separated fields from string list
190 190
191 191 Allows quick awk-like usage of string lists.
192 192
193 193 Example data (in var a, created by 'a = !ls -l')::
194 194 -rwxrwxrwx 1 ville None 18 Dec 14 2006 ChangeLog
195 195 drwxrwxrwx+ 6 ville None 0 Oct 24 18:05 IPython
196 196
197 197 a.fields(0) is ['-rwxrwxrwx', 'drwxrwxrwx+']
198 198 a.fields(1,0) is ['1 -rwxrwxrwx', '6 drwxrwxrwx+']
199 199 (note the joining by space).
200 200 a.fields(-1) is ['ChangeLog', 'IPython']
201 201
202 202 IndexErrors are ignored.
203 203
204 204 Without args, fields() just split()'s the strings.
205 205 """
206 206 if len(fields) == 0:
207 207 return [el.split() for el in self]
208 208
209 209 res = SList()
210 210 for el in [f.split() for f in self]:
211 211 lineparts = []
212 212
213 213 for fd in fields:
214 214 try:
215 215 lineparts.append(el[fd])
216 216 except IndexError:
217 217 pass
218 218 if lineparts:
219 219 res.append(" ".join(lineparts))
220 220
221 221 return res
222 222
223 223 def sort(self,field= None, nums = False):
224 224 """ sort by specified fields (see fields())
225 225
226 226 Example::
227 227 a.sort(1, nums = True)
228 228
229 229 Sorts a by second field, in numerical order (so that 21 > 3)
230 230
231 231 """
232 232
233 233 #decorate, sort, undecorate
234 234 if field is not None:
235 235 dsu = [[SList([line]).fields(field), line] for line in self]
236 236 else:
237 237 dsu = [[line, line] for line in self]
238 238 if nums:
239 239 for i in range(len(dsu)):
240 240 numstr = "".join([ch for ch in dsu[i][0] if ch.isdigit()])
241 241 try:
242 242 n = int(numstr)
243 243 except ValueError:
244 244 n = 0;
245 245 dsu[i][0] = n
246 246
247 247
248 248 dsu.sort()
249 249 return SList([t[1] for t in dsu])
250 250
251 251
252 252 # FIXME: We need to reimplement type specific displayhook and then add this
253 253 # back as a custom printer. This should also be moved outside utils into the
254 254 # core.
255 255
256 256 # def print_slist(arg):
257 257 # """ Prettier (non-repr-like) and more informative printer for SList """
258 258 # print "SList (.p, .n, .l, .s, .grep(), .fields(), sort() available):"
259 259 # if hasattr(arg, 'hideonce') and arg.hideonce:
260 260 # arg.hideonce = False
261 261 # return
262 262 #
263 263 # nlprint(arg)
264 264 #
265 265 # print_slist = result_display.when_type(SList)(print_slist)
266 266
267 267
268 268 def esc_quotes(strng):
269 269 """Return the input string with single and double quotes escaped out"""
270 270
271 271 return strng.replace('"','\\"').replace("'","\\'")
272 272
273 273
274 274 def make_quoted_expr(s):
275 275 """Return string s in appropriate quotes, using raw string if possible.
276 276
277 277 XXX - example removed because it caused encoding errors in documentation
278 278 generation. We need a new example that doesn't contain invalid chars.
279 279
280 280 Note the use of raw string and padding at the end to allow trailing
281 281 backslash.
282 282 """
283 283
284 284 tail = ''
285 285 tailpadding = ''
286 286 raw = ''
287 287 ucode = 'u'
288 288 if "\\" in s:
289 289 raw = 'r'
290 290 if s.endswith('\\'):
291 291 tail = '[:-1]'
292 292 tailpadding = '_'
293 293 if '"' not in s:
294 294 quote = '"'
295 295 elif "'" not in s:
296 296 quote = "'"
297 297 elif '"""' not in s and not s.endswith('"'):
298 298 quote = '"""'
299 299 elif "'''" not in s and not s.endswith("'"):
300 300 quote = "'''"
301 301 else:
302 302 # give up, backslash-escaped string will do
303 303 return '"%s"' % esc_quotes(s)
304 304 res = ucode + raw + quote + s + tailpadding + quote + tail
305 305 return res
306 306
307 307
308 308 def qw(words,flat=0,sep=None,maxsplit=-1):
309 309 """Similar to Perl's qw() operator, but with some more options.
310 310
311 311 qw(words,flat=0,sep=' ',maxsplit=-1) -> words.split(sep,maxsplit)
312 312
313 313 words can also be a list itself, and with flat=1, the output will be
314 314 recursively flattened.
315 315
316 316 Examples:
317 317
318 318 >>> qw('1 2')
319 319 ['1', '2']
320 320
321 321 >>> qw(['a b','1 2',['m n','p q']])
322 322 [['a', 'b'], ['1', '2'], [['m', 'n'], ['p', 'q']]]
323 323
324 324 >>> qw(['a b','1 2',['m n','p q']],flat=1)
325 325 ['a', 'b', '1', '2', 'm', 'n', 'p', 'q']
326 326 """
327 327
328 328 if isinstance(words, basestring):
329 329 return [word.strip() for word in words.split(sep,maxsplit)
330 330 if word and not word.isspace() ]
331 331 if flat:
332 332 return flatten(map(qw,words,[1]*len(words)))
333 333 return map(qw,words)
334 334
335 335
336 336 def qwflat(words,sep=None,maxsplit=-1):
337 337 """Calls qw(words) in flat mode. It's just a convenient shorthand."""
338 338 return qw(words,1,sep,maxsplit)
339 339
340 340
341 341 def qw_lol(indata):
342 342 """qw_lol('a b') -> [['a','b']],
343 343 otherwise it's just a call to qw().
344 344
345 345 We need this to make sure the modules_some keys *always* end up as a
346 346 list of lists."""
347 347
348 348 if isinstance(indata, basestring):
349 349 return [qw(indata)]
350 350 else:
351 351 return qw(indata)
352 352
353 353
354 354 def grep(pat,list,case=1):
355 355 """Simple minded grep-like function.
356 356 grep(pat,list) returns occurrences of pat in list, None on failure.
357 357
358 358 It only does simple string matching, with no support for regexps. Use the
359 359 option case=0 for case-insensitive matching."""
360 360
361 361 # This is pretty crude. At least it should implement copying only references
362 362 # to the original data in case it's big. Now it copies the data for output.
363 363 out=[]
364 364 if case:
365 365 for term in list:
366 366 if term.find(pat)>-1: out.append(term)
367 367 else:
368 368 lpat=pat.lower()
369 369 for term in list:
370 370 if term.lower().find(lpat)>-1: out.append(term)
371 371
372 372 if len(out): return out
373 373 else: return None
374 374
375 375
376 376 def dgrep(pat,*opts):
377 377 """Return grep() on dir()+dir(__builtins__).
378 378
379 379 A very common use of grep() when working interactively."""
380 380
381 381 return grep(pat,dir(__main__)+dir(__main__.__builtins__),*opts)
382 382
383 383
384 384 def idgrep(pat):
385 385 """Case-insensitive dgrep()"""
386 386
387 387 return dgrep(pat,0)
388 388
389 389
390 390 def igrep(pat,list):
391 391 """Synonym for case-insensitive grep."""
392 392
393 393 return grep(pat,list,case=0)
394 394
395 395
396 396 def indent(instr,nspaces=4, ntabs=0, flatten=False):
397 397 """Indent a string a given number of spaces or tabstops.
398 398
399 399 indent(str,nspaces=4,ntabs=0) -> indent str by ntabs+nspaces.
400 400
401 401 Parameters
402 402 ----------
403 403
404 404 instr : basestring
405 405 The string to be indented.
406 406 nspaces : int (default: 4)
407 407 The number of spaces to be indented.
408 408 ntabs : int (default: 0)
409 409 The number of tabs to be indented.
410 410 flatten : bool (default: False)
411 411 Whether to scrub existing indentation. If True, all lines will be
412 412 aligned to the same indentation. If False, existing indentation will
413 413 be strictly increased.
414 414
415 415 Returns
416 416 -------
417 417
418 418 str|unicode : string indented by ntabs and nspaces.
419 419
420 420 """
421 421 if instr is None:
422 422 return
423 423 ind = '\t'*ntabs+' '*nspaces
424 424 if flatten:
425 425 pat = re.compile(r'^\s*', re.MULTILINE)
426 426 else:
427 427 pat = re.compile(r'^', re.MULTILINE)
428 428 outstr = re.sub(pat, ind, instr)
429 429 if outstr.endswith(os.linesep+ind):
430 430 return outstr[:-len(ind)]
431 431 else:
432 432 return outstr
433 433
434 434 def native_line_ends(filename,backup=1):
435 435 """Convert (in-place) a file to line-ends native to the current OS.
436 436
437 437 If the optional backup argument is given as false, no backup of the
438 438 original file is left. """
439 439
440 440 backup_suffixes = {'posix':'~','dos':'.bak','nt':'.bak','mac':'.bak'}
441 441
442 442 bak_filename = filename + backup_suffixes[os.name]
443 443
444 444 original = open(filename).read()
445 445 shutil.copy2(filename,bak_filename)
446 446 try:
447 447 new = open(filename,'wb')
448 448 new.write(os.linesep.join(original.splitlines()))
449 449 new.write(os.linesep) # ALWAYS put an eol at the end of the file
450 450 new.close()
451 451 except:
452 452 os.rename(bak_filename,filename)
453 453 if not backup:
454 454 try:
455 455 os.remove(bak_filename)
456 456 except:
457 457 pass
458 458
459 459
460 460 def list_strings(arg):
461 461 """Always return a list of strings, given a string or list of strings
462 462 as input.
463 463
464 464 :Examples:
465 465
466 466 In [7]: list_strings('A single string')
467 467 Out[7]: ['A single string']
468 468
469 469 In [8]: list_strings(['A single string in a list'])
470 470 Out[8]: ['A single string in a list']
471 471
472 472 In [9]: list_strings(['A','list','of','strings'])
473 473 Out[9]: ['A', 'list', 'of', 'strings']
474 474 """
475 475
476 476 if isinstance(arg,basestring): return [arg]
477 477 else: return arg
478 478
479 479
480 480 def marquee(txt='',width=78,mark='*'):
481 481 """Return the input string centered in a 'marquee'.
482 482
483 483 :Examples:
484 484
485 485 In [16]: marquee('A test',40)
486 486 Out[16]: '**************** A test ****************'
487 487
488 488 In [17]: marquee('A test',40,'-')
489 489 Out[17]: '---------------- A test ----------------'
490 490
491 491 In [18]: marquee('A test',40,' ')
492 492 Out[18]: ' A test '
493 493
494 494 """
495 495 if not txt:
496 496 return (mark*width)[:width]
497 497 nmark = (width-len(txt)-2)/len(mark)/2
498 498 if nmark < 0: nmark =0
499 499 marks = mark*nmark
500 500 return '%s %s %s' % (marks,txt,marks)
501 501
502 502
503 503 ini_spaces_re = re.compile(r'^(\s+)')
504 504
505 505 def num_ini_spaces(strng):
506 506 """Return the number of initial spaces in a string"""
507 507
508 508 ini_spaces = ini_spaces_re.match(strng)
509 509 if ini_spaces:
510 510 return ini_spaces.end()
511 511 else:
512 512 return 0
513 513
514 514
515 515 def format_screen(strng):
516 516 """Format a string for screen printing.
517 517
518 518 This removes some latex-type format codes."""
519 519 # Paragraph continue
520 520 par_re = re.compile(r'\\$',re.MULTILINE)
521 521 strng = par_re.sub('',strng)
522 522 return strng
523 523
524 524 def dedent(text):
525 525 """Equivalent of textwrap.dedent that ignores unindented first line.
526 526
527 527 This means it will still dedent strings like:
528 528 '''foo
529 529 is a bar
530 530 '''
531 531
532 532 For use in wrap_paragraphs.
533 533 """
534 534
535 535 if text.startswith('\n'):
536 536 # text starts with blank line, don't ignore the first line
537 537 return textwrap.dedent(text)
538 538
539 539 # split first line
540 540 splits = text.split('\n',1)
541 541 if len(splits) == 1:
542 542 # only one line
543 543 return textwrap.dedent(text)
544 544
545 545 first, rest = splits
546 546 # dedent everything but the first line
547 547 rest = textwrap.dedent(rest)
548 548 return '\n'.join([first, rest])
549 549
550 550 def wrap_paragraphs(text, ncols=80):
551 551 """Wrap multiple paragraphs to fit a specified width.
552 552
553 553 This is equivalent to textwrap.wrap, but with support for multiple
554 554 paragraphs, as separated by empty lines.
555 555
556 556 Returns
557 557 -------
558 558
559 559 list of complete paragraphs, wrapped to fill `ncols` columns.
560 560 """
561 561 paragraph_re = re.compile(r'\n(\s*\n)+', re.MULTILINE)
562 562 text = dedent(text).strip()
563 563 paragraphs = paragraph_re.split(text)[::2] # every other entry is space
564 564 out_ps = []
565 565 indent_re = re.compile(r'\n\s+', re.MULTILINE)
566 566 for p in paragraphs:
567 567 # presume indentation that survives dedent is meaningful formatting,
568 568 # so don't fill unless text is flush.
569 569 if indent_re.search(p) is None:
570 570 # wrap paragraph
571 571 p = textwrap.fill(p, ncols)
572 572 out_ps.append(p)
573 573 return out_ps
574 574
575 575
576 576
577 577 class EvalFormatter(Formatter):
578 578 """A String Formatter that allows evaluation of simple expressions.
579 579
580 580 Any time a format key is not found in the kwargs,
581 581 it will be tried as an expression in the kwargs namespace.
582 582
583 583 This is to be used in templating cases, such as the parallel batch
584 584 script templates, where simple arithmetic on arguments is useful.
585 585
586 586 Examples
587 587 --------
588 588
589 589 In [1]: f = EvalFormatter()
590 590 In [2]: f.format('{n/4}', n=8)
591 591 Out[2]: '2'
592 592
593 593 In [3]: f.format('{range(3)}')
594 594 Out[3]: '[0, 1, 2]'
595 595
596 596 In [4]: f.format('{3*2}')
597 597 Out[4]: '6'
598 598 """
599 599
600 600 def get_value(self, key, args, kwargs):
601 601 if isinstance(key, (int, long)):
602 602 return args[key]
603 603 elif key in kwargs:
604 604 return kwargs[key]
605 605 else:
606 606 # evaluate the expression using kwargs as namespace
607 607 try:
608 608 return eval(key, kwargs)
609 609 except Exception:
610 610 # classify all bad expressions as key errors
611 611 raise KeyError(key)
612 612
613 613
614 614 def columnize(items, separator=' ', displaywidth=80):
615 615 """ Transform a list of strings into a single string with columns.
616 616
617 617 Parameters
618 618 ----------
619 619 items : sequence of strings
620 620 The strings to process.
621 621
622 622 separator : str, optional [default is two spaces]
623 623 The string that separates columns.
624 624
625 625 displaywidth : int, optional [default is 80]
626 626 Width of the display in number of characters.
627 627
628 628 Returns
629 629 -------
630 630 The formatted string.
631 631 """
632 632 # Note: this code is adapted from columnize 0.3.2.
633 633 # See http://code.google.com/p/pycolumnize/
634 634
635 635 # Some degenerate cases.
636 636 size = len(items)
637 637 if size == 0:
638 638 return '\n'
639 639 elif size == 1:
640 640 return '%s\n' % items[0]
641 641
642 # Special case: if any item is longer than the maximum width, there's no
643 # point in triggering the logic below...
644 item_len = map(len, items) # save these, we can reuse them below
645 longest = max(item_len)
646 if longest >= displaywidth:
647 return '\n'.join(items+[''])
648
642 649 # Try every row count from 1 upwards
643 650 array_index = lambda nrows, row, col: nrows*col + row
644 651 for nrows in range(1, size):
645 652 ncols = (size + nrows - 1) // nrows
646 653 colwidths = []
647 654 totwidth = -len(separator)
648 655 for col in range(ncols):
649 656 # Get max column width for this column
650 657 colwidth = 0
651 658 for row in range(nrows):
652 659 i = array_index(nrows, row, col)
653 660 if i >= size: break
654 x = items[i]
655 colwidth = max(colwidth, len(x))
661 x, len_x = items[i], item_len[i]
662 colwidth = max(colwidth, len_x)
656 663 colwidths.append(colwidth)
657 664 totwidth += colwidth + len(separator)
658 665 if totwidth > displaywidth:
659 666 break
660 667 if totwidth <= displaywidth:
661 668 break
662 669
663 670 # The smallest number of rows computed and the max widths for each
664 671 # column has been obtained. Now we just have to format each of the rows.
665 672 string = ''
666 673 for row in range(nrows):
667 674 texts = []
668 675 for col in range(ncols):
669 676 i = row + nrows*col
670 677 if i >= size:
671 678 texts.append('')
672 679 else:
673 680 texts.append(items[i])
674 681 while texts and not texts[-1]:
675 682 del texts[-1]
676 683 for col in range(len(texts)):
677 684 texts[col] = texts[col].ljust(colwidths[col])
678 685 string += '%s\n' % separator.join(texts)
679 686 return string
General Comments 0
You need to be logged in to leave comments. Login now