Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Ast transfomers #2301

Merged
merged 10 commits into from

6 participants

@takluyver
Owner

This is the next step towards IPEP 2 (#2293). It provides a mechanism that third parties can register an instance of a subclass of ast.NodeTransformer - or anything that quacks the same way, although the docstring doesn't currently mention that. It will be applied to the REPL input before executing it.

For now, transformers are expected to catch their own errors. If an error leaks out of a transformer, it is disabled, and a warning printed. We could relax this restriction in future.

In normal use, with no transformers defined, it should have negligible impact on performance, as we were already using the AST, so there's just one extra no-op function call. Edit: there's now also a call to ast.fix_missing_locations() - we could skip this with an if when there are no transformations active.

Updated: I've added tests for %timeit, %time and macros with a transformation. Macros are transformed when they are run, not when they are defined, in keeping with the idea that they're like copying the code in place. Adapting %timeit required dropping the use of timeit.template - instead, it constructs an AST equivalent of the template and adds the transformed code into that.

@travisbot

This pull request passes (merged a910db8 into 06a7a57).

@takluyver
Owner

Apologies, this has acquired a few unrelated changes - I made a small change to IPython.utils.warn, then realised I should update half a dozen places where it is called.

@travisbot

This pull request passes (merged ea09ed9 into 06a7a57).

@takluyver
Owner

I've added a test for the Integer() wrapper that both SAGE and SymPy want to use this for. This is what the transformer looks like:

class IntegerWrapper(ast.NodeTransformer):
    """Wraps all integers in a call to Integer()"""
    def visit_Num(self, node):
        if isinstance(node.n, int):
            return ast.Call(func=ast.Name(id='Integer', ctx=ast.Load()),
                            args=[node], keywords=[])
@tomspur

When a transformer threw an error, wouldn't be be better to ignore it in the current transformation only and not for all subsequent transformations?

It's similar to what we do with hooks elsewhere - something that leaks errors is considered misbehaving, and dropped. It's deliberately conservative for now, but it could be relaxed later if there's a good reason to.

I could just imagine, that a transformer fails in one transformation, but works again later on, so it'd be better to leave it in place.
But misbehaving once and nuking it out, makes sense too.

@asmeurer

I'm playing around with this, to see how it would look like for isympy. So far, I haven't gotten it to work. I just get

WARNING: AST transformer <sympy.interactive.session.SymbolWrapper object at 0x112449cd0> threw an error. It will be unregistered.

This isn't very helpful. Is there a way to see the full traceback?

@asmeurer

By the way, do you plan to update the documentation here? A specific example of, for example, how to use the Integer wrapper transformer (or another transformer if you think it would be more instructive) would be quite helpful.

@asmeurer

I think your Integer example is wrong. From the ast docs: "If the return value of the visitor method is None, the node will be removed from its location, otherwise it is replaced with the return value. The return value may be the original node in which case no replacement takes place." So I think you need an additional line at the bottom, "return node". Otherwise, something like 1.2 will fail.

@asmeurer

How efficient will this be for multiple transformers? Is it worth the time, either for me as a user or possibly for IPython, to attempt to combine disjoint transformers, so that the nodes are only walked once?

@asmeurer

On the topic of ast transformers, does anyone know of any good documentation/tutorials for this? The official Python docs are very sparse, and don't explain what many things are (for example, what does Param mean?). I'm trying to figure out how to write a transformer to wrap undefined names with Symbol(), but visit_Name is not enough (I think) because it needs to not transform something like the a in a = 1.

@asmeurer

If you use your IntegerWrapper, without the return node, you get something like

In [1]: 1.2
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/sw/lib/python2.7/codeop.pyc in __call__(self, source, filename, symbol)
    131 
    132     def __call__(self, source, filename, symbol):
--> 133         codeob = compile(source, filename, symbol, self.flags, 1)
    134         for feature in _features:
    135             if codeob.co_flags & feature.compiler_flag:

TypeError: required field "value" missing from Expr

I wonder if IPython should somehow catch this as well, and unload the transformer in that case too. It happens at a very specific location in the code (namely, when it tries to compile the empty ast), so I think it should be possible.

@takluyver
Owner

Thanks, good catch with the Integer wrapper, I've fixed it.

  • Documentation: yes, I'll put this example in as part of the IPEP2 work. It might be in a separate PR.
  • Efficiency: IPython certainly shouldn't combine transformers, because one transformation might interfere with anoter. If you're sure it's safe, you can combine your own, but it sounds like premature optimisation. I doubt the performance gain will be noticeable.
  • I don't know of any good documentation, but it looks like the .ctx attribute of a Name shows whether it's being used to store or load a variable:
In [5]: ast.dump(ast.parse("a = b"))
Out[5]: "Module(body=[Assign(targets=[Name(id='a', ctx=Store())], value=Name(id='b', ctx=Load()))])"
@takluyver
Owner

Hmm, that's a good point with the error from producing an invalid AST. It's not trivial to fix, though - we don't know which transformer introduced the error if there's more than one loaded. Maybe we can check the AST after each transformation has been applied.

@asmeurer

Efficiency: IPython certainly shouldn't combine transformers, because one transformation might interfere with anoter. If you're sure it's safe, you can combine your own, but it sounds like premature optimisation. I doubt the performance gain will be noticeable.

Ah, you're right. I was thinking it would be safe to combine transformers with disjoint visit_* methods, but clearly that is not the case, because each method could replace the node with an arbitrary other node. In fact, the tricky problem might be to make sure your transformers are injected in the right place, if you want to combine two different apps that use them. But that is not IPython's problem.

For SymPy, if we want to use both the integer and undefined name wrappers, it will be trivial to make a subclass of both transformers, and use that as the transformer instead.

And you're right that it probably wouldn't be noticeable, unless you have hundreds of transformers for some reason (or an insanely large input).

@asmeurer

I think I figured out the basic idea for the Symbol transformer. It still has some issues, but the basic stuff works.

@takluyver
Owner
@asmeurer

Yeah, I completely agree with you.

@asmeurer

In case anyone is curious, my work so far at rewriting isympy -I to use this is at https://github.com/asmeurer/sympy/tree/ast. The Symbol transformer is broken, because it's not actually as simple as wrapping all undefined names in Symbol, because that does things like Symbol('a') = 1, f(Symbol('a')=1), and x.Symbol('a') (instead of a = 1, f(a=1), and x.a). Do you think wrapping undefined names would be a common use-case? If so, maybe it would be nice to add a helper to IPython itself to do this, since it's clearly not easy to get right.

@takluyver
Owner

In cases like a=1, the Name node should have ctx=Store() - and I see your transformer already checks that ctx.__class__ == Load, so that shouldn't be an issue. The other cases (f(a=1) and x.a) don't generate a Name node for a:

In [2]: %%dump_ast
   ...: b.a
   ...: 
Module(body=[
    Expr(value=Attribute(value=Name(id='b', ctx=Load()), attr='a', ctx=Load())),
  ])

In [3]: %%dump_ast
   ...: f(a=1)
   ...: 
Module(body=[
    Expr(value=Call(func=Name(id='f', ctx=Load()), args=[], keywords=[
        keyword(arg='a', value=Num(n=1)),
      ], starargs=None, kwargs=None)),
  ])

So I'm not sure why it would break in any of those cases.

If it's useful to you, I've started a guide to working with ASTs: http://greentreesnakes.readthedocs.org/en/latest/

@asmeurer

So I'm not sure why it would break in any of those cases.

Maybe I am thinking more of issues with tokenize than with ast. Even so, there are bugs still. I guess they are more related to not overriding already defined names.

If it's useful to you, I've started a guide to working with ASTs: http://greentreesnakes.readthedocs.org/en/latest/

Great. I'll keep that in mind the next time I do some ast hacking.

@takluyver
Owner

I can imagine there would be problems if you define and use a variable in the same cell, because it won't be in the namespace when you get the AST. I think that's hard to deal with precisely, because of scoping rules and so on, but it should be possible to do a solution that works in enough cases, by scanning for variable definitions.

@asmeurer

I didn't even think of that. I personally only use the terminal, with one-line commands 99% of the time. I'll keep that in mind.

Maybe I could just create a magic function that returns the object if it is already defined and a Symbol otherwise and wrap everything as obj_or_symbol('name').

@takluyver
Owner
@asmeurer

Yes, it already doesn't work inside functions, and I didn't plan to make that work. It's designed for easy interactive use, which usually doesn't involve defining many functions/classes. The main issue is multiline cells, like for loops. Or with the notebook, literally anything can be grouped together in a cell.

I'd also add a search for var(), which is SymPy's hacky function that injects symbols into the namespace. It's hackish, but still less so than the current solution (catching NameError).

@takluyver
Owner

As currently written, I think it will act inside function definitions, although I haven't tested. A NodeTransformer does walk into function and class definitions by default. If you want to prevent that, you can override visit_FunctionDef and visit_ClassDef:

def visit_FunctionDef(self, node):
    return node

(The key is that it's not calling self.generic_visit, which implements visiting the child nodes)

@takluyver
Owner

I had an idea about your use case. Instead of turning undefined variables into Symbol('a'), you could scan for variables that aren't yet defined, then insert an Assign node at the top of the cell to create them. So 2**y would become:

y = Symbol('y')
2**y

Then if it's defined by any means in that cell, the new definition will replace the automatically created symbol. The only case I can see it would break is if the code is looking for a NameError, as in:

try:
    unicode
except NameError:
    unicode = str

I might be inclined to only create automatic symbols for single-character variables, to reduce potential collisions like that. But that's up to you.

@asmeurer

We still have to check if the name has been imported or not.

@takluyver
Owner
@jasongrout

I'm curious what the status of this pull request is. Do you see a lot more work that needs to be done on it? I'd like to start working on making Sage transforms use it if it's relatively stable.

@takluyver
Owner

I'm quite happy with the shape of this. Ideally, I'd like @fperez to have a brief look at it before I merge, but I fully intend to get it into 0.14, and I don't think anyone has objected yet.

@bfroehle

@takluyver Nothing here seems objectionable to me, but I'm not familiar enough (yet) with these mechanisms to form a solid opinion.

@asmeurer

From what I've seen and used of this so far, everything seems fine from me.

@takluyver
Owner

Thanks guys. I'm going to merge this now. It's possible that the API will still change before release.

@fperez - if you want to discuss any aspect of this post-merge, let me know. The same goes for anyone else, of course.

@takluyver takluyver merged commit a7cc583 into from
@jasongrout

One thing that is common for us to do is to not only transform integers to Integer() calls, but we also pull these calls out of subexpressions as an optimization step. For example:

sage: print sage.misc.preparser.preparse_file("""for i in range(20):
    print i+1
""")
....: 
_sage_const_20 = Integer(20); _sage_const_1 = Integer(1)
for i in range(_sage_const_20 ):
    print i+_sage_const_1 

I'm curious if you can see an easy way to do this with your AST transformers. It seems that since we are really just visiting a node at a time, it's hard to do massive reorganizations of the AST, like adding some new nodes at the top of the code.

@takluyver
Owner

It's certainly possible. The top-level node is an ast.Module, so you should be able to override visit_Module something like this:

def visit_Module(self, node):
    self.integer_consts = []
    self.generic_visit(node)   # This walks through the descendants, calling methods such as visit_Num, which you also override
    for const in self.integer_consts:
        const_assign = assign_new_const(const)
        node.body.insert(0, const_assign)
    return node
@jasongrout

Ah, great! Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
View
2  IPython/core/history.py
@@ -137,7 +137,7 @@ def __init__(self, profile='default', hist_file=u'', config=None, **traits):
self.hist_file = self._get_hist_file_name(profile)
if sqlite3 is None and self.enabled:
- warn("IPython History requires SQLite, your history will not be saved\n")
+ warn("IPython History requires SQLite, your history will not be saved")
self.enabled = False
if sqlite3 is not None:
View
38 IPython/core/interactiveshell.py
@@ -199,6 +199,13 @@ class InteractiveShell(SingletonConfigurable):
"""An enhanced, interactive shell for Python."""
_instance = None
+
+ ast_transformers = List([], config=True, help=
+ """
+ A list of ast.NodeTransformer subclass instances, which will be applied
+ to user input before code is run.
+ """
+ )
autocall = Enum((0,1,2), default_value=0, config=True, help=
"""
@@ -329,7 +336,7 @@ def _prompt_trait_changed(self, name, old, new):
'prompt_out' : 'out_template',
'prompts_pad_left' : 'justify',
}
- warn("InteractiveShell.{name} is deprecated, use PromptManager.{newname}\n".format(
+ warn("InteractiveShell.{name} is deprecated, use PromptManager.{newname}".format(
name=name, newname=table[name])
)
# protect against weird cases where self.config may not exist:
@@ -714,7 +721,7 @@ def init_virtualenv(self):
return
warn("Attempting to work in a virtualenv. If you encounter problems, please "
- "install IPython inside the virtualenv.\n")
+ "install IPython inside the virtualenv.")
if sys.platform == "win32":
virtual_env = os.path.join(os.environ['VIRTUAL_ENV'], 'Lib', 'site-packages')
else:
@@ -2630,6 +2637,8 @@ def run_cell(self, raw_cell, store_history=False, silent=False):
self.execution_count += 1
return None
+ code_ast = self.transform_ast(code_ast)
+
interactivity = "none" if silent else self.ast_node_interactivity
self.run_ast_nodes(code_ast.body, cell_name,
interactivity=interactivity)
@@ -2662,6 +2671,31 @@ def run_cell(self, raw_cell, store_history=False, silent=False):
self.history_manager.store_output(self.execution_count)
# Each cell is a *single* input, regardless of how many lines it has
self.execution_count += 1
+
+ def transform_ast(self, node):
+ """Apply the AST transformations from self.ast_transformers
+
+ Parameters
+ ----------
+ node : ast.Node
+ The root node to be transformed. Typically called with the ast.Module
+ produced by parsing user input.
+
+ Returns
+ -------
+ An ast.Node corresponding to the node it was called with. Note that it
+ may also modify the passed object, so don't rely on references to the
+ original AST.
+ """
+ for transformer in self.ast_transformers:
+ try:
+ node = transformer.visit(node)
+ except Exception:
+ warn("AST transformer %r threw an error. It will be unregistered." % transformer)
+ self.ast_transformers.remove(transformer)
+
+ return ast.fix_missing_locations(node)
+
def run_ast_nodes(self, nodelist, cell_name, interactivity='last_expr'):
"""Run a sequence of AST nodes. The execution mode depends on the
View
80 IPython/core/magics/execution.py
@@ -14,6 +14,7 @@
# Stdlib
import __builtin__ as builtin_mod
+import ast
import bdb
import os
import sys
@@ -760,26 +761,54 @@ def timeit(self, line='', cell=None):
# but is there a better way to achieve that the code stmt has access
# to the shell namespace?
transform = self.shell.input_splitter.transform_cell
+
if cell is None:
# called as line magic
- setup = 'pass'
- stmt = timeit.reindent(transform(stmt), 8)
- else:
- setup = timeit.reindent(transform(stmt), 4)
- stmt = timeit.reindent(transform(cell), 8)
-
- # From Python 3.3, this template uses new-style string formatting.
- if sys.version_info >= (3, 3):
- src = timeit.template.format(stmt=stmt, setup=setup)
+ ast_setup = ast.parse("pass")
+ ast_stmt = ast.parse(transform(stmt))
else:
- src = timeit.template % dict(stmt=stmt, setup=setup)
+ ast_setup = ast.parse(transform(stmt))
+ ast_stmt = ast.parse(transform(cell))
+
+ ast_setup = self.shell.transform_ast(ast_setup)
+ ast_stmt = self.shell.transform_ast(ast_stmt)
+
+ # This codestring is taken from timeit.template - we fill it in as an
+ # AST, so that we can apply our AST transformations to the user code
+ # without affecting the timing code.
+ timeit_ast_template = ast.parse('def inner(_it, _timer):\n'
+ ' setup\n'
+ ' _t0 = _timer()\n'
+ ' for _i in _it:\n'
+ ' stmt\n'
+ ' _t1 = _timer()\n'
+ ' return _t1 - _t0\n')
+
+ class TimeitTemplateFiller(ast.NodeTransformer):
+ "This is quite tightly tied to the template definition above."
+ def visit_FunctionDef(self, node):
+ "Fill in the setup statement"
+ self.generic_visit(node)
+ if node.name == "inner":
+ node.body[:1] = ast_setup.body
+
+ return node
+
+ def visit_For(self, node):
+ "Fill in the statement to be timed"
+ if getattr(getattr(node.body[0], 'value', None), 'id', None) == 'stmt':
+ node.body = ast_stmt.body
+ return node
+
+ timeit_ast = TimeitTemplateFiller().visit(timeit_ast_template)
+ timeit_ast = ast.fix_missing_locations(timeit_ast)
# Track compilation time so it can be reported if too long
# Minimum time above which compilation time will be reported
tc_min = 0.1
t0 = clock()
- code = compile(src, "<magic-timeit>", "exec")
+ code = compile(timeit_ast, "<magic-timeit>", "exec")
tc = clock()-t0
ns = {}
@@ -863,20 +892,31 @@ def time(self,parameter_s, user_locals):
# fail immediately if the given expression can't be compiled
expr = self.shell.prefilter(parameter_s,False)
+
+ # Minimum time above which parse time will be reported
+ tp_min = 0.1
+
+ t0 = clock()
+ expr_ast = ast.parse(expr)
+ tp = clock()-t0
+
+ # Apply AST transformations
+ expr_ast = self.shell.transform_ast(expr_ast)
# Minimum time above which compilation time will be reported
tc_min = 0.1
- try:
+ if len(expr_ast.body)==1 and isinstance(expr_ast.body[0], ast.Expr):
mode = 'eval'
- t0 = clock()
- code = compile(expr,'<timed eval>',mode)
- tc = clock()-t0
- except SyntaxError:
+ source = '<timed eval>'
+ expr_ast = ast.Expression(expr_ast.body[0].value)
+ else:
mode = 'exec'
- t0 = clock()
- code = compile(expr,'<timed exec>',mode)
- tc = clock()-t0
+ source = '<timed exec>'
+ t0 = clock()
+ code = compile(expr_ast, source, mode)
+ tc = clock()-t0
+
# skew measurement as little as possible
glob = self.shell.user_ns
wtime = time.time
@@ -902,6 +942,8 @@ def time(self,parameter_s, user_locals):
print "Wall time: %.2f s" % wall_time
if tc > tc_min:
print "Compiler : %.2f s" % tc
+ if tp > tp_min:
+ print "Parser : %.2f s" % tp
return out
@skip_doctest
View
127 IPython/core/tests/test_interactiveshell.py
@@ -20,6 +20,7 @@
# Imports
#-----------------------------------------------------------------------------
# stdlib
+import ast
import os
import shutil
import sys
@@ -414,6 +415,132 @@ def test_extraneous_loads(self):
out = "False\nFalse\nFalse\n"
tt.ipexec_validate(self.fname, out)
+class Negator(ast.NodeTransformer):
+ """Negates all number literals in an AST."""
+ def visit_Num(self, node):
+ node.n = -node.n
+ return node
+
+class TestAstTransform(unittest.TestCase):
+ def setUp(self):
+ self.negator = Negator()
+ ip.ast_transformers.append(self.negator)
+
+ def tearDown(self):
+ ip.ast_transformers.remove(self.negator)
+
+ def test_run_cell(self):
+ with tt.AssertPrints('-34'):
+ ip.run_cell('print (12 + 22)')
+
+ # A named reference to a number shouldn't be transformed.
+ ip.user_ns['n'] = 55
+ with tt.AssertNotPrints('-55'):
+ ip.run_cell('print (n)')
+
+ def test_timeit(self):
+ called = set()
+ def f(x):
+ called.add(x)
+ ip.push({'f':f})
+
+ with tt.AssertPrints("best of "):
+ ip.run_line_magic("timeit", "-n1 f(1)")
+ self.assertEqual(called, set([-1]))
+ called.clear()
+
+ with tt.AssertPrints("best of "):
+ ip.run_cell_magic("timeit", "-n1 f(2)", "f(3)")
+ self.assertEqual(called, set([-2, -3]))
+
+ def test_time(self):
+ called = []
+ def f(x):
+ called.append(x)
+ ip.push({'f':f})
+
+ # Test with an expression
+ with tt.AssertPrints("CPU times"):
+ ip.run_line_magic("time", "f(5+9)")
+ self.assertEqual(called, [-14])
+ called[:] = []
+
+ # Test with a statement (different code path)
+ with tt.AssertPrints("CPU times"):
+ ip.run_line_magic("time", "a = f(-3 + -2)")
+ self.assertEqual(called, [5])
+
+ def test_macro(self):
+ ip.push({'a':10})
+ # The AST transformation makes this do a+=-1
+ ip.define_macro("amacro", "a+=1\nprint(a)")
+
+ with tt.AssertPrints("9"):
+ ip.run_cell("amacro")
+ with tt.AssertPrints("8"):
+ ip.run_cell("amacro")
+
+class IntegerWrapper(ast.NodeTransformer):
+ """Wraps all integers in a call to Integer()"""
+ def visit_Num(self, node):
+ if isinstance(node.n, int):
+ return ast.Call(func=ast.Name(id='Integer', ctx=ast.Load()),
+ args=[node], keywords=[])
+ return node
+
+class TestAstTransform2(unittest.TestCase):
+ def setUp(self):
+ self.intwrapper = IntegerWrapper()
+ ip.ast_transformers.append(self.intwrapper)
+
+ self.calls = []
+ def Integer(*args):
+ self.calls.append(args)
+ return args
+ ip.push({"Integer": Integer})
+
+ def tearDown(self):
+ ip.ast_transformers.remove(self.intwrapper)
+ del ip.user_ns['Integer']
+
+ def test_run_cell(self):
+ ip.run_cell("n = 2")
+ self.assertEqual(self.calls, [(2,)])
+
+ # This shouldn't throw an error
+ ip.run_cell("o = 2.0")
+ self.assertEqual(ip.user_ns['o'], 2.0)
+
+ def test_timeit(self):
+ called = set()
+ def f(x):
+ called.add(x)
+ ip.push({'f':f})
+
+ with tt.AssertPrints("best of "):
+ ip.run_line_magic("timeit", "-n1 f(1)")
+ self.assertEqual(called, set([(1,)]))
+ called.clear()
+
+ with tt.AssertPrints("best of "):
+ ip.run_cell_magic("timeit", "-n1 f(2)", "f(3)")
+ self.assertEqual(called, set([(2,), (3,)]))
+
+class ErrorTransformer(ast.NodeTransformer):
+ """Throws an error when it sees a number."""
+ def visit_Num(self):
+ raise ValueError("test")
+
+class TestAstTransformError(unittest.TestCase):
+ def test_unregistering(self):
+ err_transformer = ErrorTransformer()
+ ip.ast_transformers.append(err_transformer)
+
+ with tt.AssertPrints("unregister", channel='stderr'):
+ ip.run_cell("1 + 2")
+
+ # This should have been removed.
+ nt.assert_not_in(err_transformer, ip.ast_transformers)
def test__IPYTHON__():
# This shouldn't raise a NameError, that's all
View
2  IPython/frontend/terminal/ipapp.py
@@ -351,7 +351,7 @@ def _pylab_changed(self, name, old, new):
"""Replace --pylab='inline' with --pylab='auto'"""
if new == 'inline':
warn.warn("'inline' not available as pylab backend, "
- "using 'auto' instead.\n")
+ "using 'auto' instead.")
self.pylab = 'auto'
def start(self):
View
2  IPython/lib/inputhook.py
@@ -105,7 +105,7 @@ class InputHookManager(object):
def __init__(self):
if ctypes is None:
- warn("IPython GUI event loop requires ctypes, %gui will not be available\n")
+ warn("IPython GUI event loop requires ctypes, %gui will not be available")
return
self.PYFUNC = ctypes.PYFUNCTYPE(ctypes.c_int)
self._apps = {}
View
2  IPython/testing/iptest.py
@@ -319,7 +319,7 @@ def make_exclude():
continue
fullpath = pjoin(parent, exclusion)
if not os.path.exists(fullpath) and not glob.glob(fullpath + '.*'):
- warn("Excluding nonexistent file: %r\n" % exclusion)
+ warn("Excluding nonexistent file: %r" % exclusion)
return exclusions
View
4 IPython/utils/attic.py
@@ -130,9 +130,9 @@ def import_fail_info(mod_name,fns=None):
"""Inform load failure for a module."""
if fns == None:
- warn("Loading of %s failed.\n" % (mod_name,))
+ warn("Loading of %s failed." % (mod_name,))
else:
- warn("Loading of %s from %s failed.\n" % (fns,mod_name))
+ warn("Loading of %s from %s failed." % (fns,mod_name))
class NotGiven: pass
View
2  IPython/utils/warn.py
@@ -42,7 +42,7 @@ def warn(msg,level=2,exit_val=1):
if level>0:
header = ['','','WARNING: ','ERROR: ','FATAL ERROR: ']
- io.stderr.write('%s%s' % (header[level],msg))
+ print(header[level], msg, sep='', file=io.stderr)
if level == 4:
print('Exiting.\n', file=io.stderr)
sys.exit(exit_val)
Something went wrong with that request. Please try again.