test_prefilter.py
440 lines
| 14.6 KiB
| text/x-python
|
PythonLexer
/ test / test_prefilter.py
ville
|
r988 | """ | ||
Test which prefilter transformations get called for various input lines. | ||||
Note that this does *not* test the transformations themselves -- it's just | ||||
verifying that a particular combination of, e.g. config options and escape | ||||
chars trigger the proper handle_X transform of the input line. | ||||
Usage: run from the command line with *normal* python, not ipython: | ||||
> python test_prefilter.py | ||||
Fairly quiet output by default. Pass in -v to get everyone's favorite dots. | ||||
""" | ||||
# The prefilter always ends in a call to some self.handle_X method. We swap | ||||
# all of those out so that we can capture which one was called. | ||||
import sys | ||||
sys.path.append('..') | ||||
import IPython | ||||
import IPython.ipapi | ||||
verbose = False | ||||
if len(sys.argv) > 1: | ||||
if sys.argv[1] == '-v': | ||||
sys.argv = sys.argv[:-1] # IPython is confused by -v, apparently | ||||
verbose = True | ||||
IPython.Shell.start() | ||||
ip = IPython.ipapi.get() | ||||
# Collect failed tests + stats and print them at the end | ||||
failures = [] | ||||
num_tests = 0 | ||||
# Store the results in module vars as we go | ||||
last_line = None | ||||
handler_called = None | ||||
def install_mock_handler(name): | ||||
"""Swap out one of the IP.handle_x methods with a function which can | ||||
record which handler was called and what line was produced. The mock | ||||
handler func always returns '', which causes ipython to cease handling | ||||
the string immediately. That way, that it doesn't echo output, raise | ||||
exceptions, etc. But do note that testing multiline strings thus gets | ||||
a bit hard.""" | ||||
def mock_handler(self, line, continue_prompt=None, | ||||
pre=None,iFun=None,theRest=None, | ||||
obj=None): | ||||
#print "Inside %s with '%s'" % (name, line) | ||||
global last_line, handler_called | ||||
last_line = line | ||||
handler_called = name | ||||
return '' | ||||
mock_handler.name = name | ||||
setattr(IPython.iplib.InteractiveShell, name, mock_handler) | ||||
install_mock_handler('handle_normal') | ||||
install_mock_handler('handle_auto') | ||||
install_mock_handler('handle_magic') | ||||
install_mock_handler('handle_help') | ||||
install_mock_handler('handle_shell_escape') | ||||
install_mock_handler('handle_alias') | ||||
install_mock_handler('handle_emacs') | ||||
def reset_esc_handlers(): | ||||
"""The escape handlers are stored in a hash (as an attribute of the | ||||
InteractiveShell *instance*), so we have to rebuild that hash to get our | ||||
new handlers in there.""" | ||||
s = ip.IP | ||||
s.esc_handlers = {s.ESC_PAREN : s.handle_auto, | ||||
s.ESC_QUOTE : s.handle_auto, | ||||
s.ESC_QUOTE2 : s.handle_auto, | ||||
s.ESC_MAGIC : s.handle_magic, | ||||
s.ESC_HELP : s.handle_help, | ||||
s.ESC_SHELL : s.handle_shell_escape, | ||||
s.ESC_SH_CAP : s.handle_shell_escape, | ||||
} | ||||
reset_esc_handlers() | ||||
# This is so I don't have to quote over and over. Gotta be a better way. | ||||
handle_normal = 'handle_normal' | ||||
handle_auto = 'handle_auto' | ||||
handle_magic = 'handle_magic' | ||||
handle_help = 'handle_help' | ||||
handle_shell_escape = 'handle_shell_escape' | ||||
handle_alias = 'handle_alias' | ||||
handle_emacs = 'handle_emacs' | ||||
def check(assertion, failure_msg): | ||||
"""Check a boolean assertion and fail with a message if necessary. Store | ||||
an error essage in module-level failures list in case of failure. Print | ||||
'.' or 'F' if module var Verbose is true. | ||||
""" | ||||
global num_tests | ||||
num_tests += 1 | ||||
if assertion: | ||||
if verbose: | ||||
sys.stdout.write('.') | ||||
sys.stdout.flush() | ||||
else: | ||||
if verbose: | ||||
sys.stdout.write('F') | ||||
sys.stdout.flush() | ||||
failures.append(failure_msg) | ||||
def check_handler(expected_handler, line): | ||||
"""Verify that the expected hander was called (for the given line, | ||||
passed in for failure reporting). | ||||
Pulled out to its own function so that tests which don't use | ||||
run_handler_tests can still take advantage of it.""" | ||||
check(handler_called == expected_handler, | ||||
"Expected %s to be called for %s, " | ||||
"instead %s called" % (expected_handler, | ||||
repr(line), | ||||
handler_called)) | ||||
def run_handler_tests(h_tests): | ||||
"""Loop through a series of (input_line, handler_name) pairs, verifying | ||||
that, for each ip calls the given handler for the given line. | ||||
The verbose complaint includes the line passed in, so if that line can | ||||
include enough info to find the error, the tests are modestly | ||||
self-documenting. | ||||
""" | ||||
for ln, expected_handler in h_tests: | ||||
global handler_called | ||||
handler_called = None | ||||
ip.runlines(ln) | ||||
check_handler(expected_handler, ln) | ||||
def run_one_test(ln, expected_handler): | ||||
run_handler_tests([(ln, expected_handler)]) | ||||
# ========================================= | ||||
# Tests | ||||
# ========================================= | ||||
# Fundamental escape characters + whitespace & misc | ||||
# ================================================= | ||||
esc_handler_tests = [ | ||||
( '?thing', handle_help, ), | ||||
( 'thing?', handle_help ), # '?' can trail... | ||||
( 'thing!', handle_normal), # but only '?' can trail | ||||
( ' ?thing', handle_normal), # leading whitespace turns off esc chars | ||||
( '!ls', handle_shell_escape), | ||||
( '! true', handle_shell_escape), | ||||
( '!! true', handle_shell_escape), | ||||
( '%magic', handle_magic), | ||||
# XXX Possibly, add test for /,; once those are unhooked from %autocall | ||||
( 'emacs_mode # PYTHON-MODE', handle_emacs ), | ||||
( ' ', handle_normal), | ||||
# Trailing qmark combos. Odd special cases abound | ||||
# ! always takes priority! | ||||
( '!thing?', handle_shell_escape), | ||||
( '!thing arg?', handle_shell_escape), | ||||
( '!!thing?', handle_shell_escape), | ||||
( '!!thing arg?', handle_shell_escape), | ||||
( ' !!thing arg?', handle_shell_escape), | ||||
# For all other leading esc chars, we always trigger help | ||||
( '%cmd?', handle_help), | ||||
( '%cmd ?', handle_help), | ||||
( '/cmd?', handle_help), | ||||
( '/cmd ?', handle_help), | ||||
( ';cmd?', handle_help), | ||||
( ',cmd?', handle_help), | ||||
] | ||||
run_handler_tests(esc_handler_tests) | ||||
# Shell Escapes in Multi-line statements | ||||
# ====================================== | ||||
# | ||||
# We can't test this via runlines, since the hacked-over-for-testing | ||||
# handlers all return None, so continue_prompt never becomes true. Instead | ||||
# we drop into prefilter directly and pass in continue_prompt. | ||||
old_mls = ip.options.multi_line_specials | ||||
for ln in [ ' !ls $f multi_line_specials %s', | ||||
' !!ls $f multi_line_specials %s', # !! escapes work on mls | ||||
# Trailing ? doesn't trigger help: | ||||
' !ls $f multi_line_specials %s ?', | ||||
' !!ls $f multi_line_specials %s ?', | ||||
]: | ||||
ip.options.multi_line_specials = 1 | ||||
on_ln = ln % 'on' | ||||
ignore = ip.IP.prefilter(on_ln, continue_prompt=True) | ||||
check_handler(handle_shell_escape, on_ln) | ||||
ip.options.multi_line_specials = 0 | ||||
off_ln = ln % 'off' | ||||
ignore = ip.IP.prefilter(off_ln, continue_prompt=True) | ||||
check_handler(handle_normal, off_ln) | ||||
ip.options.multi_line_specials = old_mls | ||||
# Automagic | ||||
# ========= | ||||
# Pick one magic fun and one non_magic fun, make sure both exist | ||||
assert hasattr(ip.IP, "magic_cpaste") | ||||
assert not hasattr(ip.IP, "magic_does_not_exist") | ||||
ip.options.autocall = 0 # gotta have this off to get handle_normal | ||||
ip.options.automagic = 0 | ||||
run_handler_tests([ | ||||
# Without automagic, only shows up with explicit escape | ||||
( 'cpaste', handle_normal), | ||||
( '%cpaste', handle_magic), | ||||
( '%does_not_exist', handle_magic), | ||||
]) | ||||
ip.options.automagic = 1 | ||||
run_handler_tests([ | ||||
( 'cpaste', handle_magic), | ||||
( '%cpaste', handle_magic), | ||||
( 'does_not_exist', handle_normal), | ||||
( '%does_not_exist', handle_magic), | ||||
( 'cd /', handle_magic), | ||||
( 'cd = 2', handle_normal), | ||||
( 'r', handle_magic), | ||||
( 'r thing', handle_magic), | ||||
( 'r"str"', handle_normal), | ||||
]) | ||||
# If next elt starts with anything that could be an assignment, func call, | ||||
# etc, we don't call the magic func, unless explicitly escaped to do so. | ||||
#magic_killing_tests = [] | ||||
#for c in list('!=()<>,'): | ||||
# magic_killing_tests.append(('cpaste %s killed_automagic' % c, handle_normal)) | ||||
# magic_killing_tests.append(('%%cpaste %s escaped_magic' % c, handle_magic)) | ||||
#run_handler_tests(magic_killing_tests) | ||||
# magic on indented continuation lines -- on iff multi_line_specials == 1 | ||||
ip.options.multi_line_specials = 0 | ||||
ln = ' cpaste multi_line off kills magic' | ||||
ignore = ip.IP.prefilter(ln, continue_prompt=True) | ||||
check_handler(handle_normal, ln) | ||||
ip.options.multi_line_specials = 1 | ||||
ln = ' cpaste multi_line on enables magic' | ||||
ignore = ip.IP.prefilter(ln, continue_prompt=True) | ||||
check_handler(handle_magic, ln) | ||||
# user namespace shadows the magic one unless shell escaped | ||||
ip.user_ns['cpaste'] = 'user_ns' | ||||
run_handler_tests([ | ||||
( 'cpaste', handle_normal), | ||||
( '%cpaste', handle_magic)]) | ||||
del ip.user_ns['cpaste'] | ||||
# Check for !=() turning off .ofind | ||||
# ================================= | ||||
class AttributeMutator(object): | ||||
"""A class which will be modified on attribute access, to test ofind""" | ||||
def __init__(self): | ||||
self.called = False | ||||
def getFoo(self): self.called = True | ||||
foo = property(getFoo) | ||||
attr_mutator = AttributeMutator() | ||||
ip.to_user_ns('attr_mutator') | ||||
ip.options.autocall = 1 | ||||
run_one_test('attr_mutator.foo should mutate', handle_normal) | ||||
check(attr_mutator.called, 'ofind should be called in absence of assign characters') | ||||
for c in list('!=()<>+*/%^&|'): | ||||
attr_mutator.called = False | ||||
run_one_test('attr_mutator.foo %s should *not* mutate' % c, handle_normal) | ||||
run_one_test('attr_mutator.foo%s should *not* mutate' % c, handle_normal) | ||||
check(not attr_mutator.called, | ||||
'ofind should not be called near character %s' % c) | ||||
# Alias expansion | ||||
# =============== | ||||
# With autocall on or off, aliases should be shadowed by user, internal and | ||||
# __builtin__ namespaces | ||||
# | ||||
# XXX Can aliases have '.' in their name? With autocall off, that works, | ||||
# with autocall on, it doesn't. Hmmm. | ||||
import __builtin__ | ||||
for ac_state in [0,1]: | ||||
ip.options.autocall = ac_state | ||||
ip.IP.alias_table['alias_cmd'] = 'alias_result' | ||||
ip.IP.alias_table['alias_head.with_dot'] = 'alias_result' | ||||
run_handler_tests([ | ||||
("alias_cmd", handle_alias), | ||||
# XXX See note above | ||||
#("alias_head.with_dot unshadowed, autocall=%s" % ac_state, handle_alias), | ||||
("alias_cmd.something aliases must match whole expr", handle_normal), | ||||
("alias_cmd /", handle_alias), | ||||
]) | ||||
for ns in [ip.user_ns, ip.IP.internal_ns, __builtin__.__dict__ ]: | ||||
ns['alias_cmd'] = 'a user value' | ||||
ns['alias_head'] = 'a user value' | ||||
run_handler_tests([ | ||||
("alias_cmd", handle_normal), | ||||
("alias_head.with_dot", handle_normal)]) | ||||
del ns['alias_cmd'] | ||||
del ns['alias_head'] | ||||
ip.options.autocall = 1 | ||||
# Autocall | ||||
# ======== | ||||
# For all the tests below, 'len' is callable / 'thing' is not | ||||
# Objects which are instances of IPyAutocall are *always* autocalled | ||||
import IPython.ipapi | ||||
class Autocallable(IPython.ipapi.IPyAutocall): | ||||
def __call__(self): | ||||
return "called" | ||||
autocallable = Autocallable() | ||||
ip.to_user_ns('autocallable') | ||||
# First, with autocalling fully off | ||||
ip.options.autocall = 0 | ||||
run_handler_tests( [ | ||||
# With no escapes, no autocalling expansions happen, callable or not, | ||||
# unless the obj extends IPyAutocall | ||||
( 'len autocall_0', handle_normal), | ||||
( 'thing autocall_0', handle_normal), | ||||
( 'autocallable', handle_auto), | ||||
# With explicit escapes, callable and non-callables both get expanded, | ||||
# regardless of the %autocall setting: | ||||
( '/len autocall_0', handle_auto), | ||||
( ',len autocall_0 b0', handle_auto), | ||||
( ';len autocall_0 b0', handle_auto), | ||||
( '/thing autocall_0', handle_auto), | ||||
( ',thing autocall_0 b0', handle_auto), | ||||
( ';thing autocall_0 b0', handle_auto), | ||||
# Explicit autocall should not trigger if there is leading whitespace | ||||
( ' /len autocall_0', handle_normal), | ||||
( ' ;len autocall_0', handle_normal), | ||||
( ' ,len autocall_0', handle_normal), | ||||
( ' / len autocall_0', handle_normal), | ||||
# But should work if the whitespace comes after the esc char | ||||
( '/ len autocall_0', handle_auto), | ||||
( '; len autocall_0', handle_auto), | ||||
( ', len autocall_0', handle_auto), | ||||
( '/ len autocall_0', handle_auto), | ||||
]) | ||||
# Now, with autocall in default, 'smart' mode | ||||
ip.options.autocall = 1 | ||||
run_handler_tests( [ | ||||
# Autocalls without escapes -- only expand if it's callable | ||||
( 'len a1', handle_auto), | ||||
( 'thing a1', handle_normal), | ||||
( 'autocallable', handle_auto), | ||||
# As above, all explicit escapes generate auto-calls, callable or not | ||||
( '/len a1', handle_auto), | ||||
( ',len a1 b1', handle_auto), | ||||
( ';len a1 b1', handle_auto), | ||||
( '/thing a1', handle_auto), | ||||
( ',thing a1 b1', handle_auto), | ||||
( ';thing a1 b1', handle_auto), | ||||
# Autocalls only happen on things which look like funcs, even if | ||||
# explicitly requested. Which, in this case means they look like a | ||||
# sequence of identifiers and . attribute references. Possibly the | ||||
# second of these two should trigger handle_auto. But not for now. | ||||
( '"abc".join range(4)', handle_normal), | ||||
( '/"abc".join range(4)', handle_normal), | ||||
]) | ||||
# No tests for autocall = 2, since the extra magic there happens inside the | ||||
# handle_auto function, which our test doesn't examine. | ||||
# Note that we leave autocall in default, 1, 'smart' mode | ||||
# Autocall / Binary operators | ||||
# ========================== | ||||
# Even with autocall on, 'len in thing' won't transform. | ||||
# But ';len in thing' will | ||||
# Note, the tests below don't check for multi-char ops. It could. | ||||
# XXX % is a binary op and should be in the list, too, but fails | ||||
bin_ops = list(r'<>,&^|*/+-') + 'is not in and or'.split() | ||||
bin_tests = [] | ||||
for b in bin_ops: | ||||
bin_tests.append(('len %s binop_autocall' % b, handle_normal)) | ||||
bin_tests.append((';len %s binop_autocall' % b, handle_auto)) | ||||
bin_tests.append((',len %s binop_autocall' % b, handle_auto)) | ||||
bin_tests.append(('/len %s binop_autocall' % b, handle_auto)) | ||||
# Who loves auto-generating tests? | ||||
run_handler_tests(bin_tests) | ||||
# Possibly add tests for namespace shadowing (really ofind's business?). | ||||
# | ||||
# user > ipython internal > python builtin > alias > magic | ||||
# ============ | ||||
# Test Summary | ||||
# ============ | ||||
num_f = len(failures) | ||||
if verbose: | ||||
print "%s tests run, %s failure%s" % (num_tests, | ||||
num_f, | ||||
num_f != 1 and "s" or "") | ||||
for f in failures: | ||||
print f | ||||