##// END OF EJS Templates
Merge pull request #10570 from srinivasreddy/py2_code...
Matthias Bussonnier -
r23689:b6b9fd9a merge
parent child Browse files
Show More
@@ -1,384 +1,366 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 """
2 """
3 Paging capabilities for IPython.core
3 Paging capabilities for IPython.core
4
4
5 Notes
5 Notes
6 -----
6 -----
7
7
8 For now this uses IPython hooks, so it can't be in IPython.utils. If we can get
8 For now this uses IPython hooks, so it can't be in IPython.utils. If we can get
9 rid of that dependency, we could move it there.
9 rid of that dependency, we could move it there.
10 -----
10 -----
11 """
11 """
12
12
13 # Copyright (c) IPython Development Team.
13 # Copyright (c) IPython Development Team.
14 # Distributed under the terms of the Modified BSD License.
14 # Distributed under the terms of the Modified BSD License.
15
15
16
16
17 import os
17 import os
18 import re
18 import re
19 import sys
19 import sys
20 import tempfile
20 import tempfile
21
21
22 from io import UnsupportedOperation
22 from io import UnsupportedOperation
23
23
24 from IPython import get_ipython
24 from IPython import get_ipython
25 from IPython.core.display import display
25 from IPython.core.display import display
26 from IPython.core.error import TryNext
26 from IPython.core.error import TryNext
27 from IPython.utils.data import chop
27 from IPython.utils.data import chop
28 from IPython.utils.process import system
28 from IPython.utils.process import system
29 from IPython.utils.terminal import get_terminal_size
29 from IPython.utils.terminal import get_terminal_size
30 from IPython.utils import py3compat
30 from IPython.utils import py3compat
31
31
32
32
33 def display_page(strng, start=0, screen_lines=25):
33 def display_page(strng, start=0, screen_lines=25):
34 """Just display, no paging. screen_lines is ignored."""
34 """Just display, no paging. screen_lines is ignored."""
35 if isinstance(strng, dict):
35 if isinstance(strng, dict):
36 data = strng
36 data = strng
37 else:
37 else:
38 if start:
38 if start:
39 strng = u'\n'.join(strng.splitlines()[start:])
39 strng = u'\n'.join(strng.splitlines()[start:])
40 data = { 'text/plain': strng }
40 data = { 'text/plain': strng }
41 display(data, raw=True)
41 display(data, raw=True)
42
42
43
43
44 def as_hook(page_func):
44 def as_hook(page_func):
45 """Wrap a pager func to strip the `self` arg
45 """Wrap a pager func to strip the `self` arg
46
46
47 so it can be called as a hook.
47 so it can be called as a hook.
48 """
48 """
49 return lambda self, *args, **kwargs: page_func(*args, **kwargs)
49 return lambda self, *args, **kwargs: page_func(*args, **kwargs)
50
50
51
51
52 esc_re = re.compile(r"(\x1b[^m]+m)")
52 esc_re = re.compile(r"(\x1b[^m]+m)")
53
53
54 def page_dumb(strng, start=0, screen_lines=25):
54 def page_dumb(strng, start=0, screen_lines=25):
55 """Very dumb 'pager' in Python, for when nothing else works.
55 """Very dumb 'pager' in Python, for when nothing else works.
56
56
57 Only moves forward, same interface as page(), except for pager_cmd and
57 Only moves forward, same interface as page(), except for pager_cmd and
58 mode.
58 mode.
59 """
59 """
60 if isinstance(strng, dict):
60 if isinstance(strng, dict):
61 strng = strng.get('text/plain', '')
61 strng = strng.get('text/plain', '')
62 out_ln = strng.splitlines()[start:]
62 out_ln = strng.splitlines()[start:]
63 screens = chop(out_ln,screen_lines-1)
63 screens = chop(out_ln,screen_lines-1)
64 if len(screens) == 1:
64 if len(screens) == 1:
65 print(os.linesep.join(screens[0]))
65 print(os.linesep.join(screens[0]))
66 else:
66 else:
67 last_escape = ""
67 last_escape = ""
68 for scr in screens[0:-1]:
68 for scr in screens[0:-1]:
69 hunk = os.linesep.join(scr)
69 hunk = os.linesep.join(scr)
70 print(last_escape + hunk)
70 print(last_escape + hunk)
71 if not page_more():
71 if not page_more():
72 return
72 return
73 esc_list = esc_re.findall(hunk)
73 esc_list = esc_re.findall(hunk)
74 if len(esc_list) > 0:
74 if len(esc_list) > 0:
75 last_escape = esc_list[-1]
75 last_escape = esc_list[-1]
76 print(last_escape + os.linesep.join(screens[-1]))
76 print(last_escape + os.linesep.join(screens[-1]))
77
77
78 def _detect_screen_size(screen_lines_def):
78 def _detect_screen_size(screen_lines_def):
79 """Attempt to work out the number of lines on the screen.
79 """Attempt to work out the number of lines on the screen.
80
80
81 This is called by page(). It can raise an error (e.g. when run in the
81 This is called by page(). It can raise an error (e.g. when run in the
82 test suite), so it's separated out so it can easily be called in a try block.
82 test suite), so it's separated out so it can easily be called in a try block.
83 """
83 """
84 TERM = os.environ.get('TERM',None)
84 TERM = os.environ.get('TERM',None)
85 if not((TERM=='xterm' or TERM=='xterm-color') and sys.platform != 'sunos5'):
85 if not((TERM=='xterm' or TERM=='xterm-color') and sys.platform != 'sunos5'):
86 # curses causes problems on many terminals other than xterm, and
86 # curses causes problems on many terminals other than xterm, and
87 # some termios calls lock up on Sun OS5.
87 # some termios calls lock up on Sun OS5.
88 return screen_lines_def
88 return screen_lines_def
89
89
90 try:
90 try:
91 import termios
91 import termios
92 import curses
92 import curses
93 except ImportError:
93 except ImportError:
94 return screen_lines_def
94 return screen_lines_def
95
95
96 # There is a bug in curses, where *sometimes* it fails to properly
96 # There is a bug in curses, where *sometimes* it fails to properly
97 # initialize, and then after the endwin() call is made, the
97 # initialize, and then after the endwin() call is made, the
98 # terminal is left in an unusable state. Rather than trying to
98 # terminal is left in an unusable state. Rather than trying to
99 # check everytime for this (by requesting and comparing termios
99 # check everytime for this (by requesting and comparing termios
100 # flags each time), we just save the initial terminal state and
100 # flags each time), we just save the initial terminal state and
101 # unconditionally reset it every time. It's cheaper than making
101 # unconditionally reset it every time. It's cheaper than making
102 # the checks.
102 # the checks.
103 try:
103 try:
104 term_flags = termios.tcgetattr(sys.stdout)
104 term_flags = termios.tcgetattr(sys.stdout)
105 except termios.error as err:
105 except termios.error as err:
106 # can fail on Linux 2.6, pager_page will catch the TypeError
106 # can fail on Linux 2.6, pager_page will catch the TypeError
107 raise TypeError('termios error: {0}'.format(err))
107 raise TypeError('termios error: {0}'.format(err))
108
108
109 # Curses modifies the stdout buffer size by default, which messes
110 # up Python's normal stdout buffering. This would manifest itself
111 # to IPython users as delayed printing on stdout after having used
112 # the pager.
113 #
114 # We can prevent this by manually setting the NCURSES_NO_SETBUF
115 # environment variable. For more details, see:
116 # http://bugs.python.org/issue10144
117 NCURSES_NO_SETBUF = os.environ.get('NCURSES_NO_SETBUF', None)
118 os.environ['NCURSES_NO_SETBUF'] = ''
119
120 # Proceed with curses initialization
121 try:
109 try:
122 scr = curses.initscr()
110 scr = curses.initscr()
123 except AttributeError:
111 except AttributeError:
124 # Curses on Solaris may not be complete, so we can't use it there
112 # Curses on Solaris may not be complete, so we can't use it there
125 return screen_lines_def
113 return screen_lines_def
126
114
127 screen_lines_real,screen_cols = scr.getmaxyx()
115 screen_lines_real,screen_cols = scr.getmaxyx()
128 curses.endwin()
116 curses.endwin()
129
117
130 # Restore environment
131 if NCURSES_NO_SETBUF is None:
132 del os.environ['NCURSES_NO_SETBUF']
133 else:
134 os.environ['NCURSES_NO_SETBUF'] = NCURSES_NO_SETBUF
135
136 # Restore terminal state in case endwin() didn't.
118 # Restore terminal state in case endwin() didn't.
137 termios.tcsetattr(sys.stdout,termios.TCSANOW,term_flags)
119 termios.tcsetattr(sys.stdout,termios.TCSANOW,term_flags)
138 # Now we have what we needed: the screen size in rows/columns
120 # Now we have what we needed: the screen size in rows/columns
139 return screen_lines_real
121 return screen_lines_real
140 #print '***Screen size:',screen_lines_real,'lines x',\
122 #print '***Screen size:',screen_lines_real,'lines x',\
141 #screen_cols,'columns.' # dbg
123 #screen_cols,'columns.' # dbg
142
124
143 def pager_page(strng, start=0, screen_lines=0, pager_cmd=None):
125 def pager_page(strng, start=0, screen_lines=0, pager_cmd=None):
144 """Display a string, piping through a pager after a certain length.
126 """Display a string, piping through a pager after a certain length.
145
127
146 strng can be a mime-bundle dict, supplying multiple representations,
128 strng can be a mime-bundle dict, supplying multiple representations,
147 keyed by mime-type.
129 keyed by mime-type.
148
130
149 The screen_lines parameter specifies the number of *usable* lines of your
131 The screen_lines parameter specifies the number of *usable* lines of your
150 terminal screen (total lines minus lines you need to reserve to show other
132 terminal screen (total lines minus lines you need to reserve to show other
151 information).
133 information).
152
134
153 If you set screen_lines to a number <=0, page() will try to auto-determine
135 If you set screen_lines to a number <=0, page() will try to auto-determine
154 your screen size and will only use up to (screen_size+screen_lines) for
136 your screen size and will only use up to (screen_size+screen_lines) for
155 printing, paging after that. That is, if you want auto-detection but need
137 printing, paging after that. That is, if you want auto-detection but need
156 to reserve the bottom 3 lines of the screen, use screen_lines = -3, and for
138 to reserve the bottom 3 lines of the screen, use screen_lines = -3, and for
157 auto-detection without any lines reserved simply use screen_lines = 0.
139 auto-detection without any lines reserved simply use screen_lines = 0.
158
140
159 If a string won't fit in the allowed lines, it is sent through the
141 If a string won't fit in the allowed lines, it is sent through the
160 specified pager command. If none given, look for PAGER in the environment,
142 specified pager command. If none given, look for PAGER in the environment,
161 and ultimately default to less.
143 and ultimately default to less.
162
144
163 If no system pager works, the string is sent through a 'dumb pager'
145 If no system pager works, the string is sent through a 'dumb pager'
164 written in python, very simplistic.
146 written in python, very simplistic.
165 """
147 """
166
148
167 # for compatibility with mime-bundle form:
149 # for compatibility with mime-bundle form:
168 if isinstance(strng, dict):
150 if isinstance(strng, dict):
169 strng = strng['text/plain']
151 strng = strng['text/plain']
170
152
171 # Ugly kludge, but calling curses.initscr() flat out crashes in emacs
153 # Ugly kludge, but calling curses.initscr() flat out crashes in emacs
172 TERM = os.environ.get('TERM','dumb')
154 TERM = os.environ.get('TERM','dumb')
173 if TERM in ['dumb','emacs'] and os.name != 'nt':
155 if TERM in ['dumb','emacs'] and os.name != 'nt':
174 print(strng)
156 print(strng)
175 return
157 return
176 # chop off the topmost part of the string we don't want to see
158 # chop off the topmost part of the string we don't want to see
177 str_lines = strng.splitlines()[start:]
159 str_lines = strng.splitlines()[start:]
178 str_toprint = os.linesep.join(str_lines)
160 str_toprint = os.linesep.join(str_lines)
179 num_newlines = len(str_lines)
161 num_newlines = len(str_lines)
180 len_str = len(str_toprint)
162 len_str = len(str_toprint)
181
163
182 # Dumb heuristics to guesstimate number of on-screen lines the string
164 # Dumb heuristics to guesstimate number of on-screen lines the string
183 # takes. Very basic, but good enough for docstrings in reasonable
165 # takes. Very basic, but good enough for docstrings in reasonable
184 # terminals. If someone later feels like refining it, it's not hard.
166 # terminals. If someone later feels like refining it, it's not hard.
185 numlines = max(num_newlines,int(len_str/80)+1)
167 numlines = max(num_newlines,int(len_str/80)+1)
186
168
187 screen_lines_def = get_terminal_size()[1]
169 screen_lines_def = get_terminal_size()[1]
188
170
189 # auto-determine screen size
171 # auto-determine screen size
190 if screen_lines <= 0:
172 if screen_lines <= 0:
191 try:
173 try:
192 screen_lines += _detect_screen_size(screen_lines_def)
174 screen_lines += _detect_screen_size(screen_lines_def)
193 except (TypeError, UnsupportedOperation):
175 except (TypeError, UnsupportedOperation):
194 print(str_toprint)
176 print(str_toprint)
195 return
177 return
196
178
197 #print 'numlines',numlines,'screenlines',screen_lines # dbg
179 #print 'numlines',numlines,'screenlines',screen_lines # dbg
198 if numlines <= screen_lines :
180 if numlines <= screen_lines :
199 #print '*** normal print' # dbg
181 #print '*** normal print' # dbg
200 print(str_toprint)
182 print(str_toprint)
201 else:
183 else:
202 # Try to open pager and default to internal one if that fails.
184 # Try to open pager and default to internal one if that fails.
203 # All failure modes are tagged as 'retval=1', to match the return
185 # All failure modes are tagged as 'retval=1', to match the return
204 # value of a failed system command. If any intermediate attempt
186 # value of a failed system command. If any intermediate attempt
205 # sets retval to 1, at the end we resort to our own page_dumb() pager.
187 # sets retval to 1, at the end we resort to our own page_dumb() pager.
206 pager_cmd = get_pager_cmd(pager_cmd)
188 pager_cmd = get_pager_cmd(pager_cmd)
207 pager_cmd += ' ' + get_pager_start(pager_cmd,start)
189 pager_cmd += ' ' + get_pager_start(pager_cmd,start)
208 if os.name == 'nt':
190 if os.name == 'nt':
209 if pager_cmd.startswith('type'):
191 if pager_cmd.startswith('type'):
210 # The default WinXP 'type' command is failing on complex strings.
192 # The default WinXP 'type' command is failing on complex strings.
211 retval = 1
193 retval = 1
212 else:
194 else:
213 fd, tmpname = tempfile.mkstemp('.txt')
195 fd, tmpname = tempfile.mkstemp('.txt')
214 try:
196 try:
215 os.close(fd)
197 os.close(fd)
216 with open(tmpname, 'wt') as tmpfile:
198 with open(tmpname, 'wt') as tmpfile:
217 tmpfile.write(strng)
199 tmpfile.write(strng)
218 cmd = "%s < %s" % (pager_cmd, tmpname)
200 cmd = "%s < %s" % (pager_cmd, tmpname)
219 # tmpfile needs to be closed for windows
201 # tmpfile needs to be closed for windows
220 if os.system(cmd):
202 if os.system(cmd):
221 retval = 1
203 retval = 1
222 else:
204 else:
223 retval = None
205 retval = None
224 finally:
206 finally:
225 os.remove(tmpname)
207 os.remove(tmpname)
226 else:
208 else:
227 try:
209 try:
228 retval = None
210 retval = None
229 # if I use popen4, things hang. No idea why.
211 # if I use popen4, things hang. No idea why.
230 #pager,shell_out = os.popen4(pager_cmd)
212 #pager,shell_out = os.popen4(pager_cmd)
231 pager = os.popen(pager_cmd, 'w')
213 pager = os.popen(pager_cmd, 'w')
232 try:
214 try:
233 pager_encoding = pager.encoding or sys.stdout.encoding
215 pager_encoding = pager.encoding or sys.stdout.encoding
234 pager.write(strng)
216 pager.write(strng)
235 finally:
217 finally:
236 retval = pager.close()
218 retval = pager.close()
237 except IOError as msg: # broken pipe when user quits
219 except IOError as msg: # broken pipe when user quits
238 if msg.args == (32, 'Broken pipe'):
220 if msg.args == (32, 'Broken pipe'):
239 retval = None
221 retval = None
240 else:
222 else:
241 retval = 1
223 retval = 1
242 except OSError:
224 except OSError:
243 # Other strange problems, sometimes seen in Win2k/cygwin
225 # Other strange problems, sometimes seen in Win2k/cygwin
244 retval = 1
226 retval = 1
245 if retval is not None:
227 if retval is not None:
246 page_dumb(strng,screen_lines=screen_lines)
228 page_dumb(strng,screen_lines=screen_lines)
247
229
248
230
249 def page(data, start=0, screen_lines=0, pager_cmd=None):
231 def page(data, start=0, screen_lines=0, pager_cmd=None):
250 """Display content in a pager, piping through a pager after a certain length.
232 """Display content in a pager, piping through a pager after a certain length.
251
233
252 data can be a mime-bundle dict, supplying multiple representations,
234 data can be a mime-bundle dict, supplying multiple representations,
253 keyed by mime-type, or text.
235 keyed by mime-type, or text.
254
236
255 Pager is dispatched via the `show_in_pager` IPython hook.
237 Pager is dispatched via the `show_in_pager` IPython hook.
256 If no hook is registered, `pager_page` will be used.
238 If no hook is registered, `pager_page` will be used.
257 """
239 """
258 # Some routines may auto-compute start offsets incorrectly and pass a
240 # Some routines may auto-compute start offsets incorrectly and pass a
259 # negative value. Offset to 0 for robustness.
241 # negative value. Offset to 0 for robustness.
260 start = max(0, start)
242 start = max(0, start)
261
243
262 # first, try the hook
244 # first, try the hook
263 ip = get_ipython()
245 ip = get_ipython()
264 if ip:
246 if ip:
265 try:
247 try:
266 ip.hooks.show_in_pager(data, start=start, screen_lines=screen_lines)
248 ip.hooks.show_in_pager(data, start=start, screen_lines=screen_lines)
267 return
249 return
268 except TryNext:
250 except TryNext:
269 pass
251 pass
270
252
271 # fallback on default pager
253 # fallback on default pager
272 return pager_page(data, start, screen_lines, pager_cmd)
254 return pager_page(data, start, screen_lines, pager_cmd)
273
255
274
256
275 def page_file(fname, start=0, pager_cmd=None):
257 def page_file(fname, start=0, pager_cmd=None):
276 """Page a file, using an optional pager command and starting line.
258 """Page a file, using an optional pager command and starting line.
277 """
259 """
278
260
279 pager_cmd = get_pager_cmd(pager_cmd)
261 pager_cmd = get_pager_cmd(pager_cmd)
280 pager_cmd += ' ' + get_pager_start(pager_cmd,start)
262 pager_cmd += ' ' + get_pager_start(pager_cmd,start)
281
263
282 try:
264 try:
283 if os.environ['TERM'] in ['emacs','dumb']:
265 if os.environ['TERM'] in ['emacs','dumb']:
284 raise EnvironmentError
266 raise EnvironmentError
285 system(pager_cmd + ' ' + fname)
267 system(pager_cmd + ' ' + fname)
286 except:
268 except:
287 try:
269 try:
288 if start > 0:
270 if start > 0:
289 start -= 1
271 start -= 1
290 page(open(fname).read(),start)
272 page(open(fname).read(),start)
291 except:
273 except:
292 print('Unable to show file',repr(fname))
274 print('Unable to show file',repr(fname))
293
275
294
276
295 def get_pager_cmd(pager_cmd=None):
277 def get_pager_cmd(pager_cmd=None):
296 """Return a pager command.
278 """Return a pager command.
297
279
298 Makes some attempts at finding an OS-correct one.
280 Makes some attempts at finding an OS-correct one.
299 """
281 """
300 if os.name == 'posix':
282 if os.name == 'posix':
301 default_pager_cmd = 'less -R' # -R for color control sequences
283 default_pager_cmd = 'less -R' # -R for color control sequences
302 elif os.name in ['nt','dos']:
284 elif os.name in ['nt','dos']:
303 default_pager_cmd = 'type'
285 default_pager_cmd = 'type'
304
286
305 if pager_cmd is None:
287 if pager_cmd is None:
306 try:
288 try:
307 pager_cmd = os.environ['PAGER']
289 pager_cmd = os.environ['PAGER']
308 except:
290 except:
309 pager_cmd = default_pager_cmd
291 pager_cmd = default_pager_cmd
310
292
311 if pager_cmd == 'less' and '-r' not in os.environ.get('LESS', '').lower():
293 if pager_cmd == 'less' and '-r' not in os.environ.get('LESS', '').lower():
312 pager_cmd += ' -R'
294 pager_cmd += ' -R'
313
295
314 return pager_cmd
296 return pager_cmd
315
297
316
298
317 def get_pager_start(pager, start):
299 def get_pager_start(pager, start):
318 """Return the string for paging files with an offset.
300 """Return the string for paging files with an offset.
319
301
320 This is the '+N' argument which less and more (under Unix) accept.
302 This is the '+N' argument which less and more (under Unix) accept.
321 """
303 """
322
304
323 if pager in ['less','more']:
305 if pager in ['less','more']:
324 if start:
306 if start:
325 start_string = '+' + str(start)
307 start_string = '+' + str(start)
326 else:
308 else:
327 start_string = ''
309 start_string = ''
328 else:
310 else:
329 start_string = ''
311 start_string = ''
330 return start_string
312 return start_string
331
313
332
314
333 # (X)emacs on win32 doesn't like to be bypassed with msvcrt.getch()
315 # (X)emacs on win32 doesn't like to be bypassed with msvcrt.getch()
334 if os.name == 'nt' and os.environ.get('TERM','dumb') != 'emacs':
316 if os.name == 'nt' and os.environ.get('TERM','dumb') != 'emacs':
335 import msvcrt
317 import msvcrt
336 def page_more():
318 def page_more():
337 """ Smart pausing between pages
319 """ Smart pausing between pages
338
320
339 @return: True if need print more lines, False if quit
321 @return: True if need print more lines, False if quit
340 """
322 """
341 sys.stdout.write('---Return to continue, q to quit--- ')
323 sys.stdout.write('---Return to continue, q to quit--- ')
342 ans = msvcrt.getwch()
324 ans = msvcrt.getwch()
343 if ans in ("q", "Q"):
325 if ans in ("q", "Q"):
344 result = False
326 result = False
345 else:
327 else:
346 result = True
328 result = True
347 sys.stdout.write("\b"*37 + " "*37 + "\b"*37)
329 sys.stdout.write("\b"*37 + " "*37 + "\b"*37)
348 return result
330 return result
349 else:
331 else:
350 def page_more():
332 def page_more():
351 ans = py3compat.input('---Return to continue, q to quit--- ')
333 ans = py3compat.input('---Return to continue, q to quit--- ')
352 if ans.lower().startswith('q'):
334 if ans.lower().startswith('q'):
353 return False
335 return False
354 else:
336 else:
355 return True
337 return True
356
338
357
339
358 def snip_print(str,width = 75,print_full = 0,header = ''):
340 def snip_print(str,width = 75,print_full = 0,header = ''):
359 """Print a string snipping the midsection to fit in width.
341 """Print a string snipping the midsection to fit in width.
360
342
361 print_full: mode control:
343 print_full: mode control:
362
344
363 - 0: only snip long strings
345 - 0: only snip long strings
364 - 1: send to page() directly.
346 - 1: send to page() directly.
365 - 2: snip long strings and ask for full length viewing with page()
347 - 2: snip long strings and ask for full length viewing with page()
366
348
367 Return 1 if snipping was necessary, 0 otherwise."""
349 Return 1 if snipping was necessary, 0 otherwise."""
368
350
369 if print_full == 1:
351 if print_full == 1:
370 page(header+str)
352 page(header+str)
371 return 0
353 return 0
372
354
373 print(header, end=' ')
355 print(header, end=' ')
374 if len(str) < width:
356 if len(str) < width:
375 print(str)
357 print(str)
376 snip = 0
358 snip = 0
377 else:
359 else:
378 whalf = int((width -5)/2)
360 whalf = int((width -5)/2)
379 print(str[:whalf] + ' <...> ' + str[-whalf:])
361 print(str[:whalf] + ' <...> ' + str[-whalf:])
380 snip = 1
362 snip = 1
381 if snip and print_full == 2:
363 if snip and print_full == 2:
382 if py3compat.input(header+' Snipped. View (y/n)? [N]').lower() == 'y':
364 if py3compat.input(header+' Snipped. View (y/n)? [N]').lower() == 'y':
383 page(str)
365 page(str)
384 return snip
366 return snip
General Comments 0
You need to be logged in to leave comments. Login now