##// END OF EJS Templates
Merge pull request #4151 from takluyver/drop-alias...
Thomas Kluyver -
r12759:b1100aa2 merge
parent child Browse files
Show More
@@ -0,0 +1,41 b''
1 from IPython.utils.capture import capture_output
2
3 import nose.tools as nt
4
5 def test_alias_lifecycle():
6 name = 'test_alias1'
7 cmd = 'echo "Hello"'
8 am = _ip.alias_manager
9 am.clear_aliases()
10 am.define_alias(name, cmd)
11 assert am.is_alias(name)
12 nt.assert_equal(am.retrieve_alias(name), cmd)
13 nt.assert_in((name, cmd), am.aliases)
14
15 # Test running the alias
16 orig_system = _ip.system
17 result = []
18 _ip.system = result.append
19 try:
20 _ip.run_cell('%{}'.format(name))
21 result = [c.strip() for c in result]
22 nt.assert_equal(result, [cmd])
23 finally:
24 _ip.system = orig_system
25
26 # Test removing the alias
27 am.undefine_alias(name)
28 assert not am.is_alias(name)
29 with nt.assert_raises(ValueError):
30 am.retrieve_alias(name)
31 nt.assert_not_in((name, cmd), am.aliases)
32
33
34 def test_alias_args_error():
35 """Error expanding with wrong number of arguments"""
36 _ip.alias_manager.define_alias('parts', 'echo first %s second %s')
37 # capture stderr:
38 with capture_output() as cap:
39 _ip.run_cell('parts 1')
40
41 nt.assert_equal(cap.stderr.split(':')[0], 'UsageError') No newline at end of file
@@ -0,0 +1,3 b''
1 - The alias system has been reimplemented to use magic functions. There should be little
2 visible difference while automagics are enabled, as they are by default, but parts of the
3 :class:`~IPython.core.alias.AliasManager` API have been removed.
@@ -20,17 +20,15 b' Authors:'
20 20 # Imports
21 21 #-----------------------------------------------------------------------------
22 22
23 import __builtin__
24 import keyword
25 23 import os
26 24 import re
27 25 import sys
28 26
29 27 from IPython.config.configurable import Configurable
30 from IPython.core.splitinput import split_user_input
28 from IPython.core.error import UsageError
31 29
32 30 from IPython.utils.traitlets import List, Instance
33 from IPython.utils.warn import warn, error
31 from IPython.utils.warn import error
34 32
35 33 #-----------------------------------------------------------------------------
36 34 # Utilities
@@ -104,6 +102,70 b' class AliasError(Exception):'
104 102 class InvalidAliasError(AliasError):
105 103 pass
106 104
105 class Alias(object):
106 """Callable object storing the details of one alias.
107
108 Instances are registered as magic functions to allow use of aliases.
109 """
110
111 # Prepare blacklist
112 blacklist = {'cd','popd','pushd','dhist','alias','unalias'}
113
114 def __init__(self, shell, name, cmd):
115 self.shell = shell
116 self.name = name
117 self.cmd = cmd
118 self.nargs = self.validate()
119
120 def validate(self):
121 """Validate the alias, and return the number of arguments."""
122 if self.name in self.blacklist:
123 raise InvalidAliasError("The name %s can't be aliased "
124 "because it is a keyword or builtin." % self.name)
125 try:
126 caller = self.shell.magics_manager.magics['line'][self.name]
127 except KeyError:
128 pass
129 else:
130 if not isinstance(caller, Alias):
131 raise InvalidAliasError("The name %s can't be aliased "
132 "because it is another magic command." % self.name)
133
134 if not (isinstance(self.cmd, basestring)):
135 raise InvalidAliasError("An alias command must be a string, "
136 "got: %r" % self.cmd)
137
138 nargs = self.cmd.count('%s')
139
140 if (nargs > 0) and (self.cmd.find('%l') >= 0):
141 raise InvalidAliasError('The %s and %l specifiers are mutually '
142 'exclusive in alias definitions.')
143
144 return nargs
145
146 def __repr__(self):
147 return "<alias {} for {!r}>".format(self.name, self.cmd)
148
149 def __call__(self, rest=''):
150 cmd = self.cmd
151 nargs = self.nargs
152 # Expand the %l special to be the user's input line
153 if cmd.find('%l') >= 0:
154 cmd = cmd.replace('%l', rest)
155 rest = ''
156 if nargs==0:
157 # Simple, argument-less aliases
158 cmd = '%s %s' % (cmd, rest)
159 else:
160 # Handle aliases with positional arguments
161 args = rest.split(None, nargs)
162 if len(args) < nargs:
163 raise UsageError('Alias <%s> requires %s arguments, %s given.' %
164 (self.name, nargs, len(args)))
165 cmd = '%s %s' % (cmd % tuple(args[:nargs]),' '.join(args[nargs:]))
166
167 self.shell.system(cmd)
168
107 169 #-----------------------------------------------------------------------------
108 170 # Main AliasManager class
109 171 #-----------------------------------------------------------------------------
@@ -116,35 +178,19 b' class AliasManager(Configurable):'
116 178
117 179 def __init__(self, shell=None, **kwargs):
118 180 super(AliasManager, self).__init__(shell=shell, **kwargs)
119 self.alias_table = {}
120 self.exclude_aliases()
181 # For convenient access
182 self.linemagics = self.shell.magics_manager.magics['line']
121 183 self.init_aliases()
122 184
123 def __contains__(self, name):
124 return name in self.alias_table
125
126 @property
127 def aliases(self):
128 return [(item[0], item[1][1]) for item in self.alias_table.iteritems()]
129
130 def exclude_aliases(self):
131 # set of things NOT to alias (keywords, builtins and some magics)
132 no_alias = set(['cd','popd','pushd','dhist','alias','unalias'])
133 no_alias.update(set(keyword.kwlist))
134 no_alias.update(set(__builtin__.__dict__.keys()))
135 self.no_alias = no_alias
136
137 185 def init_aliases(self):
138 # Load default aliases
139 for name, cmd in self.default_aliases:
140 self.soft_define_alias(name, cmd)
141
142 # Load user aliases
143 for name, cmd in self.user_aliases:
186 # Load default & user aliases
187 for name, cmd in self.default_aliases + self.user_aliases:
144 188 self.soft_define_alias(name, cmd)
145 189
146 def clear_aliases(self):
147 self.alias_table.clear()
190 @property
191 def aliases(self):
192 return [(n, func.cmd) for (n, func) in self.linemagics.items()
193 if isinstance(func, Alias)]
148 194
149 195 def soft_define_alias(self, name, cmd):
150 196 """Define an alias, but don't raise on an AliasError."""
@@ -159,104 +205,33 b' class AliasManager(Configurable):'
159 205 This will raise an :exc:`AliasError` if there are validation
160 206 problems.
161 207 """
162 nargs = self.validate_alias(name, cmd)
163 self.alias_table[name] = (nargs, cmd)
208 caller = Alias(shell=self.shell, name=name, cmd=cmd)
209 self.shell.magics_manager.register_function(caller, magic_kind='line',
210 magic_name=name)
164 211
165 def undefine_alias(self, name):
166 if name in self.alias_table:
167 del self.alias_table[name]
212 def get_alias(self, name):
213 """Return an alias, or None if no alias by that name exists."""
214 aname = self.linemagics.get(name, None)
215 return aname if isinstance(aname, Alias) else None
168 216
169 def validate_alias(self, name, cmd):
170 """Validate an alias and return the its number of arguments."""
171 if name in self.no_alias:
172 raise InvalidAliasError("The name %s can't be aliased "
173 "because it is a keyword or builtin." % name)
174 if not (isinstance(cmd, basestring)):
175 raise InvalidAliasError("An alias command must be a string, "
176 "got: %r" % cmd)
177 nargs = cmd.count('%s')
178 if nargs>0 and cmd.find('%l')>=0:
179 raise InvalidAliasError('The %s and %l specifiers are mutually '
180 'exclusive in alias definitions.')
181 return nargs
217 def is_alias(self, name):
218 """Return whether or not a given name has been defined as an alias"""
219 return self.get_alias(name) is not None
182 220
183 def call_alias(self, alias, rest=''):
184 """Call an alias given its name and the rest of the line."""
185 cmd = self.transform_alias(alias, rest)
186 try:
187 self.shell.system(cmd)
188 except:
189 self.shell.showtraceback()
190
191 def transform_alias(self, alias,rest=''):
192 """Transform alias to system command string."""
193 nargs, cmd = self.alias_table[alias]
194
195 if ' ' in cmd and os.path.isfile(cmd):
196 cmd = '"%s"' % cmd
197
198 # Expand the %l special to be the user's input line
199 if cmd.find('%l') >= 0:
200 cmd = cmd.replace('%l', rest)
201 rest = ''
202 if nargs==0:
203 # Simple, argument-less aliases
204 cmd = '%s %s' % (cmd, rest)
221 def undefine_alias(self, name):
222 if self.is_alias(name):
223 del self.linemagics[name]
205 224 else:
206 # Handle aliases with positional arguments
207 args = rest.split(None, nargs)
208 if len(args) < nargs:
209 raise AliasError('Alias <%s> requires %s arguments, %s given.' %
210 (alias, nargs, len(args)))
211 cmd = '%s %s' % (cmd % tuple(args[:nargs]),' '.join(args[nargs:]))
212 return cmd
213
214 def expand_alias(self, line):
215 """ Expand an alias in the command line
225 raise ValueError('%s is not an alias' % name)
216 226
217 Returns the provided command line, possibly with the first word
218 (command) translated according to alias expansion rules.
219
220 [ipython]|16> _ip.expand_aliases("np myfile.txt")
221 <16> 'q:/opt/np/notepad++.exe myfile.txt'
222 """
223
224 pre,_,fn,rest = split_user_input(line)
225 res = pre + self.expand_aliases(fn, rest)
226 return res
227
228 def expand_aliases(self, fn, rest):
229 """Expand multiple levels of aliases:
230
231 if:
232
233 alias foo bar /tmp
234 alias baz foo
235
236 then:
237
238 baz huhhahhei -> bar /tmp huhhahhei
239 """
240 line = fn + " " + rest
241
242 done = set()
243 while 1:
244 pre,_,fn,rest = split_user_input(line, shell_line_split)
245 if fn in self.alias_table:
246 if fn in done:
247 warn("Cyclic alias definition, repeated '%s'" % fn)
248 return ""
249 done.add(fn)
250
251 l2 = self.transform_alias(fn, rest)
252 if l2 == line:
253 break
254 # ls -> ls -F should not recurse forever
255 if l2.split(None,1)[0] == line.split(None,1)[0]:
256 line = l2
257 break
258 line = l2
259 else:
260 break
261
262 return line
227 def clear_aliases(self):
228 for name, cmd in self.aliases:
229 self.undefine_alias(name)
230
231 def retrieve_alias(self, name):
232 """Retrieve the command to which an alias expands."""
233 caller = self.get_alias(name)
234 if caller:
235 return caller.cmd
236 else:
237 raise ValueError('%s is not an alias' % name)
@@ -431,8 +431,7 b' class IPCompleter(Completer):'
431 431 )
432 432
433 433 def __init__(self, shell=None, namespace=None, global_namespace=None,
434 alias_table=None, use_readline=True,
435 config=None, **kwargs):
434 use_readline=True, config=None, **kwargs):
436 435 """IPCompleter() -> completer
437 436
438 437 Return a completer object suitable for use by the readline library
@@ -450,9 +449,6 b' class IPCompleter(Completer):'
450 449 handle cases (such as IPython embedded inside functions) where
451 450 both Python scopes are visible.
452 451
453 - If alias_table is supplied, it should be a dictionary of aliases
454 to complete.
455
456 452 use_readline : bool, optional
457 453 If true, use the readline library. This completer can still function
458 454 without readline, though in that case callers must provide some extra
@@ -476,9 +472,6 b' class IPCompleter(Completer):'
476 472 # List where completion matches will be stored
477 473 self.matches = []
478 474 self.shell = shell
479 if alias_table is None:
480 alias_table = {}
481 self.alias_table = alias_table
482 475 # Regexp to split filenames with spaces in them
483 476 self.space_name_re = re.compile(r'([^\\] )')
484 477 # Hold a local ref. to glob.glob for speed
@@ -505,7 +498,6 b' class IPCompleter(Completer):'
505 498 self.matchers = [self.python_matches,
506 499 self.file_matches,
507 500 self.magic_matches,
508 self.alias_matches,
509 501 self.python_func_kw_matches,
510 502 ]
511 503
@@ -628,22 +620,6 b' class IPCompleter(Completer):'
628 620 comp += [ pre+m for m in line_magics if m.startswith(bare_text)]
629 621 return comp
630 622
631 def alias_matches(self, text):
632 """Match internal system aliases"""
633 #print 'Completer->alias_matches:',text,'lb',self.text_until_cursor # dbg
634
635 # if we are not in the first 'item', alias matching
636 # doesn't make sense - unless we are starting with 'sudo' command.
637 main_text = self.text_until_cursor.lstrip()
638 if ' ' in main_text and not main_text.startswith('sudo'):
639 return []
640 text = os.path.expanduser(text)
641 aliases = self.alias_table.keys()
642 if text == '':
643 return aliases
644 else:
645 return [a for a in aliases if a.startswith(text)]
646
647 623 def python_matches(self,text):
648 624 """Match attributes or global python names"""
649 625
@@ -470,7 +470,6 b' class InteractiveShell(SingletonConfigurable):'
470 470 # because it and init_io have to come after init_readline.
471 471 self.init_user_ns()
472 472 self.init_logger()
473 self.init_alias()
474 473 self.init_builtins()
475 474
476 475 # The following was in post_config_initialization
@@ -502,6 +501,7 b' class InteractiveShell(SingletonConfigurable):'
502 501 self.init_displayhook()
503 502 self.init_latextool()
504 503 self.init_magics()
504 self.init_alias()
505 505 self.init_logstart()
506 506 self.init_pdb()
507 507 self.init_extension_manager()
@@ -1363,9 +1363,7 b' class InteractiveShell(SingletonConfigurable):'
1363 1363 namespaces = [ ('Interactive', self.user_ns),
1364 1364 ('Interactive (global)', self.user_global_ns),
1365 1365 ('Python builtin', builtin_mod.__dict__),
1366 ('Alias', self.alias_manager.alias_table),
1367 1366 ]
1368 alias_ns = self.alias_manager.alias_table
1369 1367
1370 1368 # initialize results to 'null'
1371 1369 found = False; obj = None; ospace = None; ds = None;
@@ -1404,8 +1402,6 b' class InteractiveShell(SingletonConfigurable):'
1404 1402 # If we finish the for loop (no break), we got all members
1405 1403 found = True
1406 1404 ospace = nsname
1407 if ns == alias_ns:
1408 isalias = True
1409 1405 break # namespace loop
1410 1406
1411 1407 # Try to see if it's magic
@@ -1940,7 +1936,6 b' class InteractiveShell(SingletonConfigurable):'
1940 1936 self.Completer = IPCompleter(shell=self,
1941 1937 namespace=self.user_ns,
1942 1938 global_namespace=self.user_global_ns,
1943 alias_table=self.alias_manager.alias_table,
1944 1939 use_readline=self.has_readline,
1945 1940 parent=self,
1946 1941 )
@@ -2305,7 +2300,6 b' class InteractiveShell(SingletonConfigurable):'
2305 2300 def init_alias(self):
2306 2301 self.alias_manager = AliasManager(shell=self, parent=self)
2307 2302 self.configurables.append(self.alias_manager)
2308 self.ns_table['alias'] = self.alias_manager.alias_table,
2309 2303
2310 2304 #-------------------------------------------------------------------------
2311 2305 # Things related to extensions
@@ -26,6 +26,7 b' from pprint import pformat'
26 26 from IPython.core import magic_arguments
27 27 from IPython.core import oinspect
28 28 from IPython.core import page
29 from IPython.core.alias import AliasError, Alias
29 30 from IPython.core.error import UsageError
30 31 from IPython.core.magic import (
31 32 Magics, compress_dhist, magics_class, line_magic, cell_magic, line_cell_magic
@@ -113,10 +114,14 b' class OSMagics(Magics):'
113 114 # Now try to define a new one
114 115 try:
115 116 alias,cmd = par.split(None, 1)
116 except:
117 print oinspect.getdoc(self.alias)
118 else:
119 self.shell.alias_manager.soft_define_alias(alias, cmd)
117 except TypeError:
118 print(oinspect.getdoc(self.alias))
119 return
120
121 try:
122 self.shell.alias_manager.define_alias(alias, cmd)
123 except AliasError as e:
124 print(e)
120 125 # end magic_alias
121 126
122 127 @line_magic
@@ -124,7 +129,12 b' class OSMagics(Magics):'
124 129 """Remove an alias"""
125 130
126 131 aname = parameter_s.strip()
127 self.shell.alias_manager.undefine_alias(aname)
132 try:
133 self.shell.alias_manager.undefine_alias(aname)
134 except ValueError as e:
135 print(e)
136 return
137
128 138 stored = self.shell.db.get('stored_aliases', {} )
129 139 if aname in stored:
130 140 print "Removing %stored alias",aname
@@ -182,7 +192,7 b' class OSMagics(Magics):'
182 192 try:
183 193 # Removes dots from the name since ipython
184 194 # will assume names with dots to be python.
185 if ff not in self.shell.alias_manager:
195 if not self.shell.alias_manager.is_alias(ff):
186 196 self.shell.alias_manager.define_alias(
187 197 ff.replace('.',''), ff)
188 198 except InvalidAliasError:
@@ -190,7 +200,7 b' class OSMagics(Magics):'
190 200 else:
191 201 syscmdlist.append(ff)
192 202 else:
193 no_alias = self.shell.alias_manager.no_alias
203 no_alias = Alias.blacklist
194 204 for pdir in path:
195 205 os.chdir(pdir)
196 206 for ff in os.listdir(pdir):
@@ -488,22 +488,6 b' class AutoMagicChecker(PrefilterChecker):'
488 488 return self.prefilter_manager.get_handler_by_name('magic')
489 489
490 490
491 class AliasChecker(PrefilterChecker):
492
493 priority = Integer(800, config=True)
494
495 def check(self, line_info):
496 "Check if the initital identifier on the line is an alias."
497 # Note: aliases can not contain '.'
498 head = line_info.ifun.split('.',1)[0]
499 if line_info.ifun not in self.shell.alias_manager \
500 or head not in self.shell.alias_manager \
501 or is_shadowed(head, self.shell):
502 return None
503
504 return self.prefilter_manager.get_handler_by_name('alias')
505
506
507 491 class PythonOpsChecker(PrefilterChecker):
508 492
509 493 priority = Integer(900, config=True)
@@ -591,20 +575,6 b' class PrefilterHandler(Configurable):'
591 575 return "<%s(name=%s)>" % (self.__class__.__name__, self.handler_name)
592 576
593 577
594 class AliasHandler(PrefilterHandler):
595
596 handler_name = Unicode('alias')
597
598 def handle(self, line_info):
599 """Handle alias input lines. """
600 transformed = self.shell.alias_manager.expand_aliases(line_info.ifun,line_info.the_rest)
601 # pre is needed, because it carries the leading whitespace. Otherwise
602 # aliases won't work in indented sections.
603 line_out = '%sget_ipython().system(%r)' % (line_info.pre_whitespace, transformed)
604
605 return line_out
606
607
608 578 class MacroHandler(PrefilterHandler):
609 579 handler_name = Unicode("macro")
610 580
@@ -730,14 +700,12 b' _default_checkers = ['
730 700 IPyAutocallChecker,
731 701 AssignmentChecker,
732 702 AutoMagicChecker,
733 AliasChecker,
734 703 PythonOpsChecker,
735 704 AutocallChecker
736 705 ]
737 706
738 707 _default_handlers = [
739 708 PrefilterHandler,
740 AliasHandler,
741 709 MacroHandler,
742 710 MagicHandler,
743 711 AutoHandler,
@@ -45,28 +45,6 b' def run(tests):'
45 45
46 46
47 47 def test_handlers():
48 # alias expansion
49
50 # We're using 'true' as our syscall of choice because it doesn't
51 # write anything to stdout.
52
53 # Turn off actual execution of aliases, because it's noisy
54 old_system_cmd = ip.system
55 ip.system = lambda cmd: None
56
57
58 ip.alias_manager.alias_table['an_alias'] = (0, 'true')
59 # These are useful for checking a particular recursive alias issue
60 ip.alias_manager.alias_table['top'] = (0, 'd:/cygwin/top')
61 ip.alias_manager.alias_table['d'] = (0, 'true')
62 run([(i,py3compat.u_format(o)) for i,o in \
63 [("an_alias", "get_ipython().system({u}'true ')"), # alias
64 # Below: recursive aliases should expand whitespace-surrounded
65 # chars, *not* initial chars which happen to be aliases:
66 ("top", "get_ipython().system({u}'d:/cygwin/top ')"),
67 ]])
68 ip.system = old_system_cmd
69
70 48 call_idx = CallableIndexable()
71 49 ip.user_ns['call_idx'] = call_idx
72 50
@@ -106,17 +106,6 b' class InteractiveShellTestCase(unittest.TestCase):'
106 106 ip.run_cell('a = """\n%exit\n"""')
107 107 self.assertEqual(ip.user_ns['a'], '\n%exit\n')
108 108
109 def test_alias_crash(self):
110 """Errors in prefilter can't crash IPython"""
111 ip.run_cell('%alias parts echo first %s second %s')
112 # capture stderr:
113 save_err = io.stderr
114 io.stderr = StringIO()
115 ip.run_cell('parts 1')
116 err = io.stderr.getvalue()
117 io.stderr = save_err
118 self.assertEqual(err.split(':')[0], 'ERROR')
119
120 109 def test_trailing_newline(self):
121 110 """test that running !(command) does not raise a SyntaxError"""
122 111 ip.run_cell('!(true)\n', False)
@@ -48,16 +48,16 b' class DummyMagics(magic.Magics): pass'
48 48 def test_rehashx():
49 49 # clear up everything
50 50 _ip = get_ipython()
51 _ip.alias_manager.alias_table.clear()
51 _ip.alias_manager.clear_aliases()
52 52 del _ip.db['syscmdlist']
53 53
54 54 _ip.magic('rehashx')
55 55 # Practically ALL ipython development systems will have more than 10 aliases
56 56
57 nt.assert_true(len(_ip.alias_manager.alias_table) > 10)
58 for key, val in _ip.alias_manager.alias_table.iteritems():
57 nt.assert_true(len(_ip.alias_manager.aliases) > 10)
58 for name, cmd in _ip.alias_manager.aliases:
59 59 # we must strip dots from alias names
60 nt.assert_not_in('.', key)
60 nt.assert_not_in('.', name)
61 61
62 62 # rehashx must fill up syscmdlist
63 63 scoms = _ip.db['syscmdlist']
@@ -210,17 +210,17 b' class StoreMagics(Magics, Configurable):'
210 210 obj = ip.user_ns[args[0]]
211 211 except KeyError:
212 212 # it might be an alias
213 # This needs to be refactored to use the new AliasManager stuff.
214 if args[0] in ip.alias_manager:
215 name = args[0]
216 nargs, cmd = ip.alias_manager.alias_table[ name ]
217 staliases = db.get('stored_aliases',{})
218 staliases[ name ] = cmd
219 db['stored_aliases'] = staliases
220 print "Alias stored: %s (%s)" % (name, cmd)
221 return
222 else:
223 raise UsageError("Unknown variable '%s'" % args[0])
213 name = args[0]
214 try:
215 cmd = ip.alias_manager.retrieve_alias(name)
216 except ValueError:
217 raise UsageError("Unknown variable '%s'" % name)
218
219 staliases = db.get('stored_aliases',{})
220 staliases[name] = cmd
221 db['stored_aliases'] = staliases
222 print "Alias stored: %s (%s)" % (name, cmd)
223 return
224 224
225 225 else:
226 226 modname = getattr(inspect.getmodule(obj), '__name__', '')
@@ -27,7 +27,7 b' def test_store_restore():'
27 27 # Check restoring
28 28 ip.magic('store -r')
29 29 nt.assert_equal(ip.user_ns['foo'], 78)
30 nt.assert_in('bar', ip.alias_manager.alias_table)
30 assert ip.alias_manager.is_alias('bar')
31 31 nt.assert_in(os.path.realpath(tmpd), ip.user_ns['_dh'])
32 32
33 33 os.rmdir(tmpd)
@@ -369,7 +369,7 b' class TerminalInteractiveShell(InteractiveShell):'
369 369
370 370
371 371 for name, cmd in aliases:
372 self.alias_manager.define_alias(name, cmd)
372 self.alias_manager.soft_define_alias(name, cmd)
373 373
374 374 #-------------------------------------------------------------------------
375 375 # Things related to the banner and usage
General Comments 0
You need to be logged in to leave comments. Login now