##// END OF EJS Templates
ipy_completers.quick_completer for easy creation of custom completers
vivainio -
Show More
@@ -1,46 +1,47 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """ IPython extension: add %clear magic """
2 """ IPython extension: add %clear magic """
3
3
4 import IPython.ipapi
4 import IPython.ipapi
5 import gc
5 import gc
6 ip = IPython.ipapi.get()
6 ip = IPython.ipapi.get()
7
7
8
8
9 def clear_f(self,arg):
9 def clear_f(self,arg):
10 """ Clear various data (e.g. stored history data)
10 """ Clear various data (e.g. stored history data)
11
11
12 %clear out - clear output history
12 %clear out - clear output history
13 %clear in - clear input history
13 %clear in - clear input history
14 """
14 """
15
15
16 api = self.getapi()
16 api = self.getapi()
17 for target in arg.split():
17 for target in arg.split():
18 if target == 'out':
18 if target == 'out':
19 print "Flushing output cache (%d entries)" % len(api.user_ns['_oh'])
19 print "Flushing output cache (%d entries)" % len(api.user_ns['_oh'])
20 self.outputcache.flush()
20 self.outputcache.flush()
21 elif target == 'in':
21 elif target == 'in':
22 print "Flushing input history"
22 print "Flushing input history"
23 from IPython import iplib
23 from IPython import iplib
24 del self.input_hist[:]
24 del self.input_hist[:]
25 del self.input_hist_raw[:]
25 del self.input_hist_raw[:]
26 for n in range(1,self.outputcache.prompt_count + 1):
26 for n in range(1,self.outputcache.prompt_count + 1):
27 key = '_i'+`n`
27 key = '_i'+`n`
28 try:
28 try:
29 del self.user_ns[key]
29 del self.user_ns[key]
30 except: pass
30 except: pass
31 elif target == 'array':
31 elif target == 'array':
32 try:
32 try:
33 pylab=ip.IP.pylab
33 pylab=ip.IP.pylab
34 for x in self.user_ns.keys():
34 for x in self.user_ns.keys():
35 if isinstance(self.user_ns[x],pylab.arraytype):
35 if isinstance(self.user_ns[x],pylab.arraytype):
36 del self.user_ns[x]
36 del self.user_ns[x]
37 except AttributeError:
37 except AttributeError:
38 print "Clear array only available in -pylab mode"
38 print "Clear array only available in -pylab mode"
39 gc.collect()
39 gc.collect()
40
40 elif target == 'shadow':
41 api.db.hcompress('shadowhist')
41
42
42 ip.expose_magic("clear",clear_f)
43 ip.expose_magic("clear",clear_f)
43
44
44
45
45
46
46
47
@@ -1,323 +1,346 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2
2
3 """ Implementations for various useful completers
3 """ Implementations for various useful completers
4
4
5 See Extensions/ipy_stock_completers.py on examples of how to enable a completer,
5 See Extensions/ipy_stock_completers.py on examples of how to enable a completer,
6 but the basic idea is to do:
6 but the basic idea is to do:
7
7
8 ip.set_hook('complete_command', svn_completer, str_key = 'svn')
8 ip.set_hook('complete_command', svn_completer, str_key = 'svn')
9
9
10 """
10 """
11 import IPython.ipapi
11 import IPython.ipapi
12 import glob,os,shlex,sys
12 import glob,os,shlex,sys
13 import inspect
13 import inspect
14 from time import time
14 from time import time
15 ip = IPython.ipapi.get()
15 ip = IPython.ipapi.get()
16
16
17 TIMEOUT_STORAGE = 3 #Time in seconds after which the rootmodules will be stored
17 TIMEOUT_STORAGE = 3 #Time in seconds after which the rootmodules will be stored
18 TIMEOUT_GIVEUP = 20 #Time in seconds after which we give up
18 TIMEOUT_GIVEUP = 20 #Time in seconds after which we give up
19
19
20 def quick_completer(cmd, completions):
21 """ Easily create a completer a trivial completer for a command.
22
23 Takes either a list of completions, or all completions in string
24 (that will be split on whitespace)
25
26 Example::
27
28 [d:\ipython]|1> import ipy_completers
29 [d:\ipython]|2> ipy_completers.quick_completer('foo', ['bar','baz'])
30 [d:\ipython]|3> foo boooo
31
32 bar baz
33 [d:\ipython]|3> foo ba
34 """
35 if isinstance(completions, basestring):
36
37 completions = completions.split()
38 def do_complete(self,event):
39 return completions
40
41 ip.set_hook('complete_command',do_complete, str_key = cmd)
42
20 def getRootModules():
43 def getRootModules():
21 """
44 """
22 Returns a list containing the names of all the modules available in the
45 Returns a list containing the names of all the modules available in the
23 folders of the pythonpath.
46 folders of the pythonpath.
24 """
47 """
25 modules = []
48 modules = []
26 if ip.db.has_key('rootmodules'):
49 if ip.db.has_key('rootmodules'):
27 return ip.db['rootmodules']
50 return ip.db['rootmodules']
28 t = time()
51 t = time()
29 store = False
52 store = False
30 for path in sys.path:
53 for path in sys.path:
31 modules += moduleList(path)
54 modules += moduleList(path)
32 if time() - t >= TIMEOUT_STORAGE and not store:
55 if time() - t >= TIMEOUT_STORAGE and not store:
33 store = True
56 store = True
34 print "\nCaching the list of root modules, please wait!"
57 print "\nCaching the list of root modules, please wait!"
35 print "(This will only be done once - type '%rehashx' to " + \
58 print "(This will only be done once - type '%rehashx' to " + \
36 "reset cache!)"
59 "reset cache!)"
37 print
60 print
38 if time() - t > TIMEOUT_GIVEUP:
61 if time() - t > TIMEOUT_GIVEUP:
39 print "This is taking too long, we give up."
62 print "This is taking too long, we give up."
40 print
63 print
41 ip.db['rootmodules'] = []
64 ip.db['rootmodules'] = []
42 return []
65 return []
43
66
44 modules += sys.builtin_module_names
67 modules += sys.builtin_module_names
45 modules = list(set(modules))
68 modules = list(set(modules))
46 if '__init__' in modules:
69 if '__init__' in modules:
47 modules.remove('__init__')
70 modules.remove('__init__')
48 modules = list(set(modules))
71 modules = list(set(modules))
49 if store:
72 if store:
50 ip.db['rootmodules'] = modules
73 ip.db['rootmodules'] = modules
51 return modules
74 return modules
52
75
53 def moduleList(path):
76 def moduleList(path):
54 """
77 """
55 Return the list containing the names of the modules available in the given
78 Return the list containing the names of the modules available in the given
56 folder.
79 folder.
57 """
80 """
58 if os.path.isdir(path):
81 if os.path.isdir(path):
59 folder_list = os.listdir(path)
82 folder_list = os.listdir(path)
60 else:
83 else:
61 folder_list = []
84 folder_list = []
62 #folder_list = glob.glob(os.path.join(path,'*'))
85 #folder_list = glob.glob(os.path.join(path,'*'))
63 folder_list = [path for path in folder_list \
86 folder_list = [path for path in folder_list \
64 if os.path.exists(os.path.join(path,'__init__.py'))\
87 if os.path.exists(os.path.join(path,'__init__.py'))\
65 or path[-3:] in ('.py','.so')\
88 or path[-3:] in ('.py','.so')\
66 or path[-4:] in ('.pyc','.pyo')]
89 or path[-4:] in ('.pyc','.pyo')]
67 folder_list += folder_list
90 folder_list += folder_list
68 folder_list = [os.path.basename(path).split('.')[0] for path in folder_list]
91 folder_list = [os.path.basename(path).split('.')[0] for path in folder_list]
69 return folder_list
92 return folder_list
70
93
71 def moduleCompletion(line):
94 def moduleCompletion(line):
72 """
95 """
73 Returns a list containing the completion possibilities for an import line.
96 Returns a list containing the completion possibilities for an import line.
74 The line looks like this :
97 The line looks like this :
75 'import xml.d'
98 'import xml.d'
76 'from xml.dom import'
99 'from xml.dom import'
77 """
100 """
78 def tryImport(mod, only_modules=False):
101 def tryImport(mod, only_modules=False):
79 def isImportable(module, attr):
102 def isImportable(module, attr):
80 if only_modules:
103 if only_modules:
81 return inspect.ismodule(getattr(module, attr))
104 return inspect.ismodule(getattr(module, attr))
82 else:
105 else:
83 return not(attr[:2] == '__' and attr[-2:] == '__')
106 return not(attr[:2] == '__' and attr[-2:] == '__')
84 try:
107 try:
85 m = __import__(mod)
108 m = __import__(mod)
86 except:
109 except:
87 return []
110 return []
88 mods = mod.split('.')
111 mods = mod.split('.')
89 for module in mods[1:]:
112 for module in mods[1:]:
90 m = getattr(m,module)
113 m = getattr(m,module)
91 if (not hasattr(m, '__file__')) or (not only_modules) or\
114 if (not hasattr(m, '__file__')) or (not only_modules) or\
92 (hasattr(m, '__file__') and '__init__' in m.__file__):
115 (hasattr(m, '__file__') and '__init__' in m.__file__):
93 completion_list = [attr for attr in dir(m) if isImportable(m, attr)]
116 completion_list = [attr for attr in dir(m) if isImportable(m, attr)]
94 completion_list.extend(getattr(m,'__all__',[]))
117 completion_list.extend(getattr(m,'__all__',[]))
95 if hasattr(m, '__file__') and '__init__' in m.__file__:
118 if hasattr(m, '__file__') and '__init__' in m.__file__:
96 completion_list.extend(moduleList(os.path.dirname(m.__file__)))
119 completion_list.extend(moduleList(os.path.dirname(m.__file__)))
97 completion_list = list(set(completion_list))
120 completion_list = list(set(completion_list))
98 if '__init__' in completion_list:
121 if '__init__' in completion_list:
99 completion_list.remove('__init__')
122 completion_list.remove('__init__')
100 return completion_list
123 return completion_list
101
124
102 words = line.split(' ')
125 words = line.split(' ')
103 if len(words) == 3 and words[0] == 'from':
126 if len(words) == 3 and words[0] == 'from':
104 return ['import ']
127 return ['import ']
105 if len(words) < 3 and (words[0] in ['import','from']) :
128 if len(words) < 3 and (words[0] in ['import','from']) :
106 if len(words) == 1:
129 if len(words) == 1:
107 return getRootModules()
130 return getRootModules()
108 mod = words[1].split('.')
131 mod = words[1].split('.')
109 if len(mod) < 2:
132 if len(mod) < 2:
110 return getRootModules()
133 return getRootModules()
111 completion_list = tryImport('.'.join(mod[:-1]), True)
134 completion_list = tryImport('.'.join(mod[:-1]), True)
112 completion_list = ['.'.join(mod[:-1] + [el]) for el in completion_list]
135 completion_list = ['.'.join(mod[:-1] + [el]) for el in completion_list]
113 return completion_list
136 return completion_list
114 if len(words) >= 3 and words[0] == 'from':
137 if len(words) >= 3 and words[0] == 'from':
115 mod = words[1]
138 mod = words[1]
116 return tryImport(mod)
139 return tryImport(mod)
117
140
118 def vcs_completer(commands, event):
141 def vcs_completer(commands, event):
119 """ utility to make writing typical version control app completers easier
142 """ utility to make writing typical version control app completers easier
120
143
121 VCS command line apps typically have the format:
144 VCS command line apps typically have the format:
122
145
123 [sudo ]PROGNAME [help] [command] file file...
146 [sudo ]PROGNAME [help] [command] file file...
124
147
125 """
148 """
126
149
127
150
128 cmd_param = event.line.split()
151 cmd_param = event.line.split()
129 if event.line.endswith(' '):
152 if event.line.endswith(' '):
130 cmd_param.append('')
153 cmd_param.append('')
131
154
132 if cmd_param[0] == 'sudo':
155 if cmd_param[0] == 'sudo':
133 cmd_param = cmd_param[1:]
156 cmd_param = cmd_param[1:]
134
157
135 if len(cmd_param) == 2 or 'help' in cmd_param:
158 if len(cmd_param) == 2 or 'help' in cmd_param:
136 return commands.split()
159 return commands.split()
137
160
138 return ip.IP.Completer.file_matches(event.symbol)
161 return ip.IP.Completer.file_matches(event.symbol)
139
162
140
163
141
164
142 def apt_completers(self, event):
165 def apt_completers(self, event):
143 """ This should return a list of strings with possible completions.
166 """ This should return a list of strings with possible completions.
144
167
145 Note that all the included strings that don't start with event.symbol
168 Note that all the included strings that don't start with event.symbol
146 are removed, in order to not confuse readline.
169 are removed, in order to not confuse readline.
147
170
148 """
171 """
149 # print event # dbg
172 # print event # dbg
150
173
151 # commands are only suggested for the 'command' part of package manager
174 # commands are only suggested for the 'command' part of package manager
152 # invocation
175 # invocation
153
176
154 cmd = (event.line + "<placeholder>").rsplit(None,1)[0]
177 cmd = (event.line + "<placeholder>").rsplit(None,1)[0]
155 # print cmd
178 # print cmd
156 if cmd.endswith('apt-get') or cmd.endswith('yum'):
179 if cmd.endswith('apt-get') or cmd.endswith('yum'):
157 return ['update', 'upgrade', 'install', 'remove']
180 return ['update', 'upgrade', 'install', 'remove']
158
181
159 # later on, add dpkg -l / whatever to get list of possible
182 # later on, add dpkg -l / whatever to get list of possible
160 # packages, add switches etc. for the rest of command line
183 # packages, add switches etc. for the rest of command line
161 # filling
184 # filling
162
185
163 raise IPython.ipapi.TryNext
186 raise IPython.ipapi.TryNext
164
187
165
188
166
189
167 pkg_cache = None
190 pkg_cache = None
168
191
169 def module_completer(self,event):
192 def module_completer(self,event):
170 """ Give completions after user has typed 'import ...' or 'from ...'"""
193 """ Give completions after user has typed 'import ...' or 'from ...'"""
171
194
172 # This works in all versions of python. While 2.5 has
195 # This works in all versions of python. While 2.5 has
173 # pkgutil.walk_packages(), that particular routine is fairly dangerous,
196 # pkgutil.walk_packages(), that particular routine is fairly dangerous,
174 # since it imports *EVERYTHING* on sys.path. That is: a) very slow b) full
197 # since it imports *EVERYTHING* on sys.path. That is: a) very slow b) full
175 # of possibly problematic side effects.
198 # of possibly problematic side effects.
176 # This search the folders in the sys.path for available modules.
199 # This search the folders in the sys.path for available modules.
177
200
178 return moduleCompletion(event.line)
201 return moduleCompletion(event.line)
179
202
180
203
181 svn_commands = """\
204 svn_commands = """\
182 add blame praise annotate ann cat checkout co cleanup commit ci copy
205 add blame praise annotate ann cat checkout co cleanup commit ci copy
183 cp delete del remove rm diff di export help ? h import info list ls
206 cp delete del remove rm diff di export help ? h import info list ls
184 lock log merge mkdir move mv rename ren propdel pdel pd propedit pedit
207 lock log merge mkdir move mv rename ren propdel pdel pd propedit pedit
185 pe propget pget pg proplist plist pl propset pset ps resolved revert
208 pe propget pget pg proplist plist pl propset pset ps resolved revert
186 status stat st switch sw unlock update
209 status stat st switch sw unlock update
187 """
210 """
188
211
189 def svn_completer(self,event):
212 def svn_completer(self,event):
190 return vcs_completer(svn_commands, event)
213 return vcs_completer(svn_commands, event)
191
214
192
215
193 hg_commands = """
216 hg_commands = """
194 add addremove annotate archive backout branch branches bundle cat
217 add addremove annotate archive backout branch branches bundle cat
195 clone commit copy diff export grep heads help identify import incoming
218 clone commit copy diff export grep heads help identify import incoming
196 init locate log manifest merge outgoing parents paths pull push
219 init locate log manifest merge outgoing parents paths pull push
197 qapplied qclone qcommit qdelete qdiff qfold qguard qheader qimport
220 qapplied qclone qcommit qdelete qdiff qfold qguard qheader qimport
198 qinit qnew qnext qpop qprev qpush qrefresh qrename qrestore qsave
221 qinit qnew qnext qpop qprev qpush qrefresh qrename qrestore qsave
199 qselect qseries qtop qunapplied recover remove rename revert rollback
222 qselect qseries qtop qunapplied recover remove rename revert rollback
200 root serve showconfig status strip tag tags tip unbundle update verify
223 root serve showconfig status strip tag tags tip unbundle update verify
201 version
224 version
202 """
225 """
203
226
204 def hg_completer(self,event):
227 def hg_completer(self,event):
205 """ Completer for mercurial commands """
228 """ Completer for mercurial commands """
206
229
207 return vcs_completer(hg_commands, event)
230 return vcs_completer(hg_commands, event)
208
231
209
232
210
233
211 bzr_commands = """
234 bzr_commands = """
212 add annotate bind branch break-lock bundle-revisions cat check
235 add annotate bind branch break-lock bundle-revisions cat check
213 checkout commit conflicts deleted diff export gannotate gbranch
236 checkout commit conflicts deleted diff export gannotate gbranch
214 gcommit gdiff help ignore ignored info init init-repository inventory
237 gcommit gdiff help ignore ignored info init init-repository inventory
215 log merge missing mkdir mv nick pull push reconcile register-branch
238 log merge missing mkdir mv nick pull push reconcile register-branch
216 remerge remove renames resolve revert revno root serve sign-my-commits
239 remerge remove renames resolve revert revno root serve sign-my-commits
217 status testament unbind uncommit unknowns update upgrade version
240 status testament unbind uncommit unknowns update upgrade version
218 version-info visualise whoami
241 version-info visualise whoami
219 """
242 """
220
243
221 def bzr_completer(self,event):
244 def bzr_completer(self,event):
222 """ Completer for bazaar commands """
245 """ Completer for bazaar commands """
223 cmd_param = event.line.split()
246 cmd_param = event.line.split()
224 if event.line.endswith(' '):
247 if event.line.endswith(' '):
225 cmd_param.append('')
248 cmd_param.append('')
226
249
227 if len(cmd_param) > 2:
250 if len(cmd_param) > 2:
228 cmd = cmd_param[1]
251 cmd = cmd_param[1]
229 param = cmd_param[-1]
252 param = cmd_param[-1]
230 output_file = (param == '--output=')
253 output_file = (param == '--output=')
231 if cmd == 'help':
254 if cmd == 'help':
232 return bzr_commands.split()
255 return bzr_commands.split()
233 elif cmd in ['bundle-revisions','conflicts',
256 elif cmd in ['bundle-revisions','conflicts',
234 'deleted','nick','register-branch',
257 'deleted','nick','register-branch',
235 'serve','unbind','upgrade','version',
258 'serve','unbind','upgrade','version',
236 'whoami'] and not output_file:
259 'whoami'] and not output_file:
237 return []
260 return []
238 else:
261 else:
239 # the rest are probably file names
262 # the rest are probably file names
240 return ip.IP.Completer.file_matches(event.symbol)
263 return ip.IP.Completer.file_matches(event.symbol)
241
264
242 return bzr_commands.split()
265 return bzr_commands.split()
243
266
244
267
245 def shlex_split(x):
268 def shlex_split(x):
246 """Helper function to split lines into segments."""
269 """Helper function to split lines into segments."""
247 #shlex.split raise exception if syntax error in sh syntax
270 #shlex.split raise exception if syntax error in sh syntax
248 #for example if no closing " is found. This function keeps dropping
271 #for example if no closing " is found. This function keeps dropping
249 #the last character of the line until shlex.split does not raise
272 #the last character of the line until shlex.split does not raise
250 #exception. Adds end of the line to the result of shlex.split
273 #exception. Adds end of the line to the result of shlex.split
251 #example: %run "c:/python -> ['%run','"c:/python']
274 #example: %run "c:/python -> ['%run','"c:/python']
252 endofline=[]
275 endofline=[]
253 while x!="":
276 while x!="":
254 try:
277 try:
255 comps=shlex.split(x)
278 comps=shlex.split(x)
256 if len(endofline)>=1:
279 if len(endofline)>=1:
257 comps.append("".join(endofline))
280 comps.append("".join(endofline))
258 return comps
281 return comps
259 except ValueError:
282 except ValueError:
260 endofline=[x[-1:]]+endofline
283 endofline=[x[-1:]]+endofline
261 x=x[:-1]
284 x=x[:-1]
262 return ["".join(endofline)]
285 return ["".join(endofline)]
263
286
264 def runlistpy(self, event):
287 def runlistpy(self, event):
265 comps = shlex_split(event.line)
288 comps = shlex_split(event.line)
266 relpath = (len(comps) > 1 and comps[-1] or '').strip("'\"")
289 relpath = (len(comps) > 1 and comps[-1] or '').strip("'\"")
267
290
268 #print "\nev=",event # dbg
291 #print "\nev=",event # dbg
269 #print "rp=",relpath # dbg
292 #print "rp=",relpath # dbg
270 #print 'comps=',comps # dbg
293 #print 'comps=',comps # dbg
271
294
272 lglob = glob.glob
295 lglob = glob.glob
273 isdir = os.path.isdir
296 isdir = os.path.isdir
274 if relpath.startswith('~'):
297 if relpath.startswith('~'):
275 relpath = os.path.expanduser(relpath)
298 relpath = os.path.expanduser(relpath)
276 dirs = [f.replace('\\','/') + "/" for f in lglob(relpath+'*')
299 dirs = [f.replace('\\','/') + "/" for f in lglob(relpath+'*')
277 if isdir(f)]
300 if isdir(f)]
278
301
279 # Find if the user has already typed the first filename, after which we
302 # Find if the user has already typed the first filename, after which we
280 # should complete on all files, since after the first one other files may
303 # should complete on all files, since after the first one other files may
281 # be arguments to the input script.
304 # be arguments to the input script.
282 #filter(
305 #filter(
283 if filter(lambda f: f.endswith('.py') or f.endswith('.ipy'),comps):
306 if filter(lambda f: f.endswith('.py') or f.endswith('.ipy'),comps):
284 pys = [f.replace('\\','/') for f in lglob('*')]
307 pys = [f.replace('\\','/') for f in lglob('*')]
285 else:
308 else:
286 pys = [f.replace('\\','/')
309 pys = [f.replace('\\','/')
287 for f in lglob(relpath+'*.py') + lglob(relpath+'*.ipy')]
310 for f in lglob(relpath+'*.py') + lglob(relpath+'*.ipy')]
288 return dirs + pys
311 return dirs + pys
289
312
290
313
291 def cd_completer(self, event):
314 def cd_completer(self, event):
292 relpath = event.symbol
315 relpath = event.symbol
293 #print event # dbg
316 #print event # dbg
294 if '-b' in event.line:
317 if '-b' in event.line:
295 # return only bookmark completions
318 # return only bookmark completions
296 bkms = self.db.get('bookmarks',{})
319 bkms = self.db.get('bookmarks',{})
297 return bkms.keys()
320 return bkms.keys()
298
321
299
322
300 if event.symbol == '-':
323 if event.symbol == '-':
301 # jump in directory history by number
324 # jump in directory history by number
302 ents = ['-%d [%s]' % (i,s) for i,s in enumerate(ip.user_ns['_dh'])]
325 ents = ['-%d [%s]' % (i,s) for i,s in enumerate(ip.user_ns['_dh'])]
303 if len(ents) > 1:
326 if len(ents) > 1:
304 return ents
327 return ents
305 return []
328 return []
306
329
307 if relpath.startswith('~'):
330 if relpath.startswith('~'):
308 relpath = os.path.expanduser(relpath).replace('\\','/')
331 relpath = os.path.expanduser(relpath).replace('\\','/')
309 found = []
332 found = []
310 for d in [f.replace('\\','/') + '/' for f in glob.glob(relpath+'*')
333 for d in [f.replace('\\','/') + '/' for f in glob.glob(relpath+'*')
311 if os.path.isdir(f)]:
334 if os.path.isdir(f)]:
312 if ' ' in d:
335 if ' ' in d:
313 # we don't want to deal with any of that, complex code
336 # we don't want to deal with any of that, complex code
314 # for this is elsewhere
337 # for this is elsewhere
315 raise IPython.ipapi.TryNext
338 raise IPython.ipapi.TryNext
316 found.append( d )
339 found.append( d )
317
340
318 if not found:
341 if not found:
319 if os.path.isdir(relpath):
342 if os.path.isdir(relpath):
320 return [relpath]
343 return [relpath]
321 raise IPython.ipapi.TryNext
344 raise IPython.ipapi.TryNext
322 return found
345 return found
323
346
General Comments 0
You need to be logged in to leave comments. Login now