From f53fc8efaff12efac0276c5729ef5e167c9aaaf3 2006-10-30 19:54:25 From: vivainio Date: 2006-10-30 19:54:25 Subject: [PATCH] First round of 'complete_command' hook, implements customizable command line completers --- diff --git a/IPython/Extensions/ipy_linux_package_managers.py b/IPython/Extensions/ipy_linux_package_managers.py new file mode 100755 index 0000000..924ffb3 --- /dev/null +++ b/IPython/Extensions/ipy_linux_package_managers.py @@ -0,0 +1,47 @@ +""" Tab completion support for a couple of linux package managers + +This is also an example of how to write custom completer plugins +or hooks. + +Practical use: + +[ipython]|1> import ipy_linux_package_managers +[ipython]|2> apt-get u<<< press tab here >>> +update upgrade +[ipython]|2> apt-get up + +""" +import IPython.ipapi + +ip = IPython.ipapi.get() + +def apt_completers(self, event): + """ This should return a list of strings with possible completions. + + Note that all the included strings that don't start with event.symbol + are removed, in order to not confuse readline. + + """ + # print event # dbg + + # commands are only suggested for the 'command' part of package manager + # invocation + + cmd = (event.line + "").rsplit(None,1)[0] + # print cmd + if cmd.endswith('apt-get') or cmd.endswith('yum'): + return ['update', 'upgrade', 'install', 'remove'] + + # later on, add dpkg -l / whatever to get list of possible + # packages, add switches etc. for the rest of command line + # filling + + raise IPython.ipapi.TryNext + + +# re_key specifies the regexp that triggers the specified completer + +ip.set_hook('complete_command', apt_completers, re_key = '.*apt-get') + +ip.set_hook('complete_command', apt_completers, re_key = '.*yum') + \ No newline at end of file diff --git a/IPython/__init__.py b/IPython/__init__.py index 1f70fad..a0e4038 100644 --- a/IPython/__init__.py +++ b/IPython/__init__.py @@ -27,7 +27,7 @@ IPython tries to: IPython requires Python 2.3 or newer. -$Id: __init__.py 1328 2006-05-25 07:47:56Z fperez $""" +$Id: __init__.py 1854 2006-10-30 19:54:25Z vivainio $""" #***************************************************************************** # Copyright (C) 2001-2006 Fernando Perez. @@ -51,7 +51,7 @@ sys.path.append(os.path.dirname(__file__) + "/Extensions") __all__ = ['deep_reload','genutils','ipstruct','ultraTB','DPyGetOpt', 'Itpl','hooks','ConfigLoader','OutputTrap','Release','Shell', 'platutils','platutils_win32','platutils_posix','platutils_dummy', - 'ipapi','rlineimpl'] + 'ipapi','rlineimpl', 'strdispatch'] # Load __all__ in IPython namespace so that a simple 'import IPython' gives # access to them via IPython. diff --git a/IPython/completer.py b/IPython/completer.py index 0f89170..a87d3ca 100644 --- a/IPython/completer.py +++ b/IPython/completer.py @@ -72,6 +72,8 @@ import re import shlex import sys import IPython.rlineimpl as readline +from IPython.ipstruct import Struct +from IPython import ipapi import types @@ -341,7 +343,7 @@ class IPCompleter(Completer): current (as of Python 2.3) Python readline it's possible to do better.""" - #print 'Completer->file_matches: <%s>' % text # dbg + # print 'Completer->file_matches: <%s>' % text # dbg # chars that require escaping with backslash - i.e. chars # that readline treats incorrectly as delimiters, but we @@ -526,6 +528,25 @@ class IPCompleter(Completer): argMatches.append("%s=" %namedArg) return argMatches + def dispatch_custom_completer(self,text): + # print "Custom! '%s' %s" % (text, self.custom_completers) # dbg + line = self.lbuf + event = Struct() + event.line = line + event.symbol = text + event.command = None + for c in self.custom_completers.flat_matches(self.lbuf): + # print "try",c # dbg + try: + res = c(event) + return [r for r in res if r.startswith(text)] + except ipapi.TryNext: + pass + + return None + + + def complete(self, text, state): """Return the next possible completion for 'text'. @@ -547,6 +568,7 @@ class IPCompleter(Completer): self.readline.insert_text('\t') return None + magic_escape = self.magic_escape magic_prefix = self.magic_prefix @@ -556,26 +578,31 @@ class IPCompleter(Completer): elif text.startswith('~'): text = os.path.expanduser(text) if state == 0: - # Extend the list of completions with the results of each - # matcher, so we return results to the user from all - # namespaces. - if self.merge_completions: - self.matches = [] - for matcher in self.matchers: - self.matches.extend(matcher(text)) + custom_res = self.dispatch_custom_completer(text) + if custom_res is not None: + # did custom completers produce something? + self.matches = custom_res else: - for matcher in self.matchers: - self.matches = matcher(text) - if self.matches: - break + # Extend the list of completions with the results of each + # matcher, so we return results to the user from all + # namespaces. + if self.merge_completions: + self.matches = [] + for matcher in self.matchers: + self.matches.extend(matcher(text)) + else: + for matcher in self.matchers: + self.matches = matcher(text) + if self.matches: + break try: return self.matches[state].replace(magic_prefix,magic_escape) except IndexError: return None except: - #from IPython.ultraTB import AutoFormattedTB; # dbg - #tb=AutoFormattedTB('Verbose');tb() #dbg + from IPython.ultraTB import AutoFormattedTB; # dbg + tb=AutoFormattedTB('Verbose');tb() #dbg # If completion fails, don't annoy the user. return None diff --git a/IPython/hooks.py b/IPython/hooks.py index f980c5e..a272173 100644 --- a/IPython/hooks.py +++ b/IPython/hooks.py @@ -32,7 +32,7 @@ ip.set_hook('editor', calljed) You can then enable the functionality by doing 'import myiphooks' somewhere in your configuration files or ipython command line. -$Id: hooks.py 1366 2006-06-15 19:45:50Z vivainio $""" +$Id: hooks.py 1854 2006-10-30 19:54:25Z vivainio $""" #***************************************************************************** # Copyright (C) 2005 Fernando Perez. @@ -145,6 +145,13 @@ class CommandChainDispatcher: """ Add a func to the cmd chain with given priority """ bisect.insort(self.chain,(priority,func)) + def __iter__(self): + """ Return all objects in chain. + + Handy if the objects are not callable. + """ + return iter(self.chain) + def result_display(self,arg): """ Default display hook. diff --git a/IPython/iplib.py b/IPython/iplib.py index 1265422..814d5a4 100644 --- a/IPython/iplib.py +++ b/IPython/iplib.py @@ -6,7 +6,7 @@ Requires Python 2.3 or newer. This file contains all the classes and helper functions specific to IPython. -$Id: iplib.py 1853 2006-10-30 17:00:39Z vivainio $ +$Id: iplib.py 1854 2006-10-30 19:54:25Z vivainio $ """ #***************************************************************************** @@ -72,6 +72,7 @@ from IPython.ipstruct import Struct from IPython.background_jobs import BackgroundJobManager from IPython.usage import cmd_line_usage,interactive_usage from IPython.genutils import * +from IPython.strdispatch import StrDispatch import IPython.ipapi # Globals @@ -404,6 +405,8 @@ class InteractiveShell(object,Magic): # hooks holds pointers used for user-side customizations self.hooks = Struct() + self.strdispatchers = {} + # Set all default hooks, defined in the IPython.hooks module. hooks = IPython.hooks for hook_name in hooks.__all__: @@ -737,7 +740,7 @@ class InteractiveShell(object,Magic): __builtin__.__dict__[biname] = bival self.builtins_added.clear() - def set_hook(self,name,hook, priority = 50): + def set_hook(self,name,hook, priority = 50, str_key = None, re_key = None): """set_hook(name,hook) -> sets an internal IPython hook. IPython exposes some of its internal API as user-modifiable hooks. By @@ -747,13 +750,27 @@ class InteractiveShell(object,Magic): # At some point in the future, this should validate the hook before it # accepts it. Probably at least check that the hook takes the number # of args it's supposed to. + + f = new.instancemethod(hook,self,self.__class__) + + # check if the hook is for strdispatcher first + if str_key is not None: + sdp = self.strdispatchers.get(name, StrDispatch()) + sdp.add_s(str_key, f, priority ) + self.strdispatchers[name] = sdp + return + if re_key is not None: + sdp = self.strdispatchers.get(name, StrDispatch()) + sdp.add_re(re.compile(re_key), f, priority ) + self.strdispatchers[name] = sdp + return + dp = getattr(self.hooks, name, None) if name not in IPython.hooks.__all__: print "Warning! Hook '%s' is not one of %s" % (name, IPython.hooks.__all__ ) if not dp: dp = IPython.hooks.CommandChainDispatcher() - f = new.instancemethod(hook,self,self.__class__) try: dp.add(f,priority) except AttributeError: @@ -1220,7 +1237,9 @@ want to merge them back into the new files.""" % locals() self.user_global_ns, self.rc.readline_omit__names, self.alias_table) - + sdisp = self.strdispatchers.get('complete_command', StrDispatch()) + self.strdispatchers['complete_command'] = sdisp + self.Completer.custom_completers = sdisp # Platform-specific configuration if os.name == 'nt': self.readline_startup_hook = readline.set_pre_input_hook diff --git a/IPython/strdispatch.py b/IPython/strdispatch.py new file mode 100755 index 0000000..6d51aa2 --- /dev/null +++ b/IPython/strdispatch.py @@ -0,0 +1,57 @@ +from IPython.hooks import CommandChainDispatcher +import IPython.hooks + +import re + +class StrDispatch(object): + """ Dispatch (lookup) a set of strings / regexps for match """ + def __init__(self): + self.strs = {} + self.regexs = {} + def add_s(self, s, obj, priority= 0 ): + """ Adds a target 'string' for dispatching """ + + chain = self.strs.get(s, CommandChainDispatcher()) + chain.add(obj,priority) + self.strs[s] = chain + + def add_re(self, regex, obj, priority= 0 ): + """ Adds a target regexp for dispatching """ + + chain = self.regexs.get(regex, CommandChainDispatcher()) + chain.add(obj,priority) + self.regexs[regex] = chain + + def dispatch(self, key): + """ Get a seq of Commandchain objects that match key """ + if key in self.strs: + yield self.strs[key] + + for r, obj in self.regexs.items(): + if re.match(r, key): + yield obj + else: + #print "nomatch",key + pass + + + def __repr__(self): + return "" % (self.strs, self.regexs) + def flat_matches(self, key): + """ Yield all 'value' targets, without priority """ + for val in self.dispatch(key): + for el in val: + yield el[1] # only value, no priority + return + + +def test(): + d = StrDispatch() + d.add_s('hei',34, priority = 4) + d.add_s('hei',123, priority = 2) + print list(d.dispatch('hei')) + d.add_re('h.i', 686) + print list(d.flat_matches('hei')) + +if __name__ == '__main__': + test() \ No newline at end of file diff --git a/doc/ChangeLog b/doc/ChangeLog index 6b531cf..5047ebc 100644 --- a/doc/ChangeLog +++ b/doc/ChangeLog @@ -3,6 +3,12 @@ * Debugger.py, iplib.py (debugger()): Add last set of Rocky Bernsteins's patches for pydb integration. http://bashdb.sourceforge.net/pydb/ + + * strdispatch.py, iplib.py, completer.py, IPython/__init__.py, + Extensions/ipy_linux_package_managers.py, hooks.py: Implement + custom completer hook to allow the users to implement their own + completers. See ipy_linux_package_managers.py for example. The + hook name is 'complete_command'. 2006-10-28 Fernando Perez