# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
"""tests for the astng inference capabilities
"""
import sys
from os.path import join, abspath
from logilab.common.testlib import TestCase, unittest_main
from logilab.astng import builder, nodes, inference, utils, YES, Instance, InferenceContext
def get_name_node(start_from, name, index=0):
return [n for n in start_from.nodes_of_class(nodes.Name) if n.name == name][index]
def get_node_of_class(start_from, klass):
return start_from.nodes_of_class(klass).next()
builder = builder.ASTNGBuilder()
class InferenceUtilsTC(TestCase):
def test_path_wrapper(self):
infer_default = inference.path_wrapper(inference.infer_default)
infer_end = inference.path_wrapper(inference.infer_end)
self.failUnlessRaises(inference.InferenceError,
infer_default(1).next)
self.failUnlessEqual(infer_end(1).next(), 1)
class InferenceTC(TestCase):
DATA = '''
import exceptions
class C(object):
"new style"
attr = 4
def meth1(self, arg1, optarg=0):
var = object()
print "yo", arg1, optarg
self.iattr = "hop"
return var
def meth2(self):
self.meth1(*self.meth3)
def meth3(self, d=attr):
b = self.attr
c = self.iattr
return b, c
ex = exceptions.Exception("msg")
v = C().meth1(1)
a, b, c = ex, 1, "bonjour"
[d, e, f] = [ex, 1.0, ("bonjour", v)]
g, h = f
i, (j, k) = u"glup", f
a, b= b, a # Gasp !
'''
def setUp(self):
self.astng = builder.string_build(self.DATA, __name__, __file__)
def test_module_inference(self):
infered = self.astng.infer()
obj = infered.next()
self.failUnlessEqual(obj.name, __name__)
self.failUnlessEqual(obj.root().name, __name__)
self.failUnlessRaises(StopIteration, infered.next)
def test_class_inference(self):
infered = self.astng['C'].infer()
obj = infered.next()
self.failUnlessEqual(obj.name, 'C')
self.failUnlessEqual(obj.root().name, __name__)
self.failUnlessRaises(StopIteration, infered.next)
def test_function_inference(self):
infered = self.astng['C']['meth1'].infer()
obj = infered.next()
self.failUnlessEqual(obj.name, 'meth1')
self.failUnlessEqual(obj.root().name, __name__)
self.failUnlessRaises(StopIteration, infered.next)
def test_builtin_name_inference(self):
infered = self.astng['C']['meth1']['var'].infer()
var = infered.next()
self.failUnlessEqual(var.name, 'object')
self.failUnlessEqual(var.root().name, '__builtin__')
self.failUnlessRaises(StopIteration, infered.next)
def test_tupleassign_name_inference(self):
infered = self.astng['a'].infer()
exc = infered.next()
self.failUnless(isinstance(exc, inference.Instance))
self.failUnlessEqual(exc.name, 'Exception')
self.failUnlessEqual(exc.root().name, 'exceptions')
self.failUnlessRaises(StopIteration, infered.next)
infered = self.astng['b'].infer()
const = infered.next()
self.failUnless(isinstance(const, nodes.Const))
self.failUnlessEqual(const.value, 1)
self.failUnlessRaises(StopIteration, infered.next)
infered = self.astng['c'].infer()
const = infered.next()
self.failUnless(isinstance(const, nodes.Const))
self.failUnlessEqual(const.value, "bonjour")
self.failUnlessRaises(StopIteration, infered.next)
def test_listassign_name_inference(self):
infered = self.astng['d'].infer()
exc = infered.next()
self.failUnless(isinstance(exc, inference.Instance))
self.failUnlessEqual(exc.name, 'Exception')
self.failUnlessEqual(exc.root().name, 'exceptions')
self.failUnlessRaises(StopIteration, infered.next)
infered = self.astng['e'].infer()
const = infered.next()
self.failUnless(isinstance(const, nodes.Const))
self.failUnlessEqual(const.value, 1.0)
self.failUnlessRaises(StopIteration, infered.next)
infered = self.astng['f'].infer()
const = infered.next()
self.failUnless(isinstance(const, nodes.Tuple))
self.failUnlessRaises(StopIteration, infered.next)
def test_advanced_tupleassign_name_inference1(self):
infered = self.astng['g'].infer()
const = infered.next()
self.failUnless(isinstance(const, nodes.Const))
self.failUnlessEqual(const.value, "bonjour")
self.failUnlessRaises(StopIteration, infered.next)
infered = self.astng['h'].infer()
var = infered.next()
self.failUnlessEqual(var.name, 'object')
self.failUnlessEqual(var.root().name, '__builtin__')
self.failUnlessRaises(StopIteration, infered.next)
def test_advanced_tupleassign_name_inference2(self):
infered = self.astng['i'].infer()
const = infered.next()
self.failUnless(isinstance(const, nodes.Const))
self.failUnlessEqual(const.value, u"glup")
self.failUnlessRaises(StopIteration, infered.next)
infered = self.astng['j'].infer()
const = infered.next()
self.failUnless(isinstance(const, nodes.Const))
self.failUnlessEqual(const.value, "bonjour")
self.failUnlessRaises(StopIteration, infered.next)
infered = self.astng['k'].infer()
var = infered.next()
self.failUnlessEqual(var.name, 'object')
self.failUnlessEqual(var.root().name, '__builtin__')
self.failUnlessRaises(StopIteration, infered.next)
def test_swap_assign_inference(self):
infered = self.astng.locals['a'][1].infer()
const = infered.next()
self.failUnless(isinstance(const, nodes.Const))
self.failUnlessEqual(const.value, 1)
self.failUnlessRaises(StopIteration, infered.next)
infered = self.astng.locals['b'][1].infer()
exc = infered.next()
self.failUnless(isinstance(exc, inference.Instance))
self.failUnlessEqual(exc.name, 'Exception')
self.failUnlessEqual(exc.root().name, 'exceptions')
self.failUnlessRaises(StopIteration, infered.next)
def test_getattr_inference1(self):
infered = self.astng['ex'].infer()
exc = infered.next()
self.failUnless(isinstance(exc, inference.Instance))
self.failUnlessEqual(exc.name, 'Exception')
self.failUnlessEqual(exc.root().name, 'exceptions')
self.failUnlessRaises(StopIteration, infered.next)
def test_getattr_inference2(self):
infered = get_node_of_class(self.astng['C']['meth2'], nodes.Getattr).infer()
meth1 = infered.next()
self.failUnlessEqual(meth1.name, 'meth1')
self.failUnlessEqual(meth1.root().name, __name__)
self.failUnlessRaises(StopIteration, infered.next)
def test_getattr_inference3(self):
infered = self.astng['C']['meth3']['b'].infer()
const = infered.next()
self.failUnless(isinstance(const, nodes.Const))
self.failUnlessEqual(const.value, 4)
self.failUnlessRaises(StopIteration, infered.next)
def test_getattr_inference4(self):
infered = self.astng['C']['meth3']['c'].infer()
const = infered.next()
self.failUnless(isinstance(const, nodes.Const))
self.failUnlessEqual(const.value, "hop")
self.failUnlessRaises(StopIteration, infered.next)
def test_callfunc_inference(self):
infered = self.astng['v'].infer()
meth1 = infered.next()
self.failUnless(isinstance(meth1, inference.Instance))
self.failUnlessEqual(meth1.name, 'object')
self.failUnlessEqual(meth1.root().name, '__builtin__')
self.failUnlessRaises(StopIteration, infered.next)
def test_args_default_inference1(self):
optarg = get_name_node(self.astng['C']['meth1'], 'optarg')
infered = optarg.infer()
obj1 = infered.next()
self.failUnless(isinstance(obj1, nodes.Const))
self.failUnlessEqual(obj1.value, 0)
obj1 = infered.next()
self.failUnless(obj1 is YES)
self.failUnlessRaises(StopIteration, infered.next)
def test_args_default_inference2(self):
infered = self.astng['C']['meth3'].ilookup('d')
obj1 = infered.next()
self.failUnless(isinstance(obj1, nodes.Const))
self.failUnlessEqual(obj1.value, 4)
obj1 = infered.next()
self.failUnless(obj1 is YES)
self.failUnlessRaises(StopIteration, infered.next)
def test_inference_restrictions(self):
infered = get_name_node(self.astng['C']['meth1'], 'arg1').infer()
obj1 = infered.next()
self.failUnless(obj1 is YES)
self.failUnlessRaises(StopIteration, infered.next)
def test_del(self):
data = '''
del undefined_attr
'''
astng = builder.string_build(data, __name__, __file__)
self.failUnlessRaises(inference.InferenceError,
astng.node.getChildNodes()[0].infer().next)
def test_ancestors_inference(self):
data = '''
class A:
pass
class A(A):
pass
'''
astng = builder.string_build(data, __name__, __file__)
a1 = astng.locals['A'][0]
a2 = astng.locals['A'][1]
a2_ancestors = list(a2.ancestors())
self.failUnlessEqual(len(a2_ancestors), 1)
self.failUnless(a2_ancestors[0] is a1)
def test_ancestors_inference2(self):
data = '''
class A:
pass
class B(A): pass
class A(B):
pass
'''
astng = builder.string_build(data, __name__, __file__)
a1 = astng.locals['A'][0]
a2 = astng.locals['A'][1]
a2_ancestors = list(a2.ancestors())
self.failUnlessEqual(len(a2_ancestors), 2)
self.failUnless(a2_ancestors[0] is astng.locals['B'][0])
self.failUnless(a2_ancestors[1] is a1, a2_ancestors[1])
def test_f_arg_f(self):
data = '''
def f(f=1):
return f
a = f()
'''
astng = builder.string_build(data, __name__, __file__)
a = astng['a']
a_infer = a.infer()
self.failUnlessEqual(a_infer.next().value, 1)
self.failUnlessRaises(StopIteration, a_infer.next)
def test_exc_ancestors(self):
data = '''
def f():
raise NotImplementedError
'''
astng = builder.string_build(data, __name__, __file__)
names = astng.nodes_of_class(nodes.Name)
nie = names.next().infer().next()
self.failUnless(isinstance(nie, nodes.Class))
nie_ancestors = [c.name for c in nie.ancestors()]
if sys.version_info < (2, 5):
self.failUnlessEqual(nie_ancestors, ['RuntimeError', 'StandardError', 'Exception'])
else:
self.failUnlessEqual(nie_ancestors, ['RuntimeError', 'StandardError', 'Exception', 'BaseException', 'object'])
def test_except_inference(self):
data = '''
try:
print hop
except NameError, ex:
ex1 = ex
except Exception, ex:
ex2 = ex
raise
'''
astng = builder.string_build(data, __name__, __file__)
ex1 = astng['ex1']
ex1_infer = ex1.infer()
infered = list(ex1.infer())
#print 'EX1:', ex1
#print 'INFEREND', infered
ex1 = ex1_infer.next()
self.failUnless(isinstance(ex1, inference.Instance))
self.failUnlessEqual(ex1.name, 'NameError')
self.failUnlessRaises(StopIteration, ex1_infer.next)
ex2 = astng['ex2']
ex2_infer = ex2.infer()
ex2 = ex2_infer.next()
self.failUnless(isinstance(ex2, inference.Instance))
self.failUnlessEqual(ex2.name, 'Exception')
self.failUnlessRaises(StopIteration, ex2_infer.next)
def test_del(self):
data = '''
a = 1
b = a
del a
c = a
a = 2
d = a
'''
astng = builder.string_build(data, __name__, __file__)
n = astng['b']
n_infer = n.infer()
infered = n_infer.next()
self.failUnless(isinstance(infered, nodes.Const))
self.failUnlessEqual(infered.value, 1)
self.failUnlessRaises(StopIteration, n_infer.next)
n = astng['c']
n_infer = n.infer()
self.failUnlessRaises(inference.InferenceError, n_infer.next)
n = astng['d']
n_infer = n.infer()
infered = n_infer.next()
self.failUnless(isinstance(infered, nodes.Const))
self.failUnlessEqual(infered.value, 2)
self.failUnlessRaises(StopIteration, n_infer.next)
def test_builtin_types(self):
data = '''
l = [1]
t = (2,)
d = {}
s = ''
u = u''
'''
astng = builder.string_build(data, __name__, __file__)
n = astng['l']
infered = n.infer().next()
self.failUnless(isinstance(infered, nodes.List))
self.failUnless(isinstance(infered, inference.Instance))
self.failUnlessEqual(infered.getitem(0).value, 1)
self.failUnless(isinstance(infered._proxied, nodes.Class))
self.failUnlessEqual(infered._proxied.name, 'list')
self.failUnless('append' in infered._proxied.locals)
n = astng['t']
infered = n.infer().next()
self.failUnless(isinstance(infered, nodes.Tuple))
self.failUnless(isinstance(infered, inference.Instance))
self.failUnlessEqual(infered.getitem(0).value, 2)
self.failUnless(isinstance(infered._proxied, nodes.Class))
self.failUnlessEqual(infered._proxied.name, 'tuple')
n = astng['d']
infered = n.infer().next()
self.failUnless(isinstance(infered, nodes.Dict))
self.failUnless(isinstance(infered, inference.Instance))
self.failUnless(isinstance(infered._proxied, nodes.Class))
self.failUnlessEqual(infered._proxied.name, 'dict')
self.failUnless('get' in infered._proxied.locals)
n = astng['s']
infered = n.infer().next()
self.failUnless(isinstance(infered, nodes.Const))
self.failUnless(isinstance(infered, inference.Instance))
self.failUnlessEqual(infered.name, 'str')
self.failUnless('lower' in infered._proxied.locals)
n = astng['u']
infered = n.infer().next()
self.failUnless(isinstance(infered, nodes.Const))
self.failUnless(isinstance(infered, inference.Instance))
self.failUnlessEqual(infered.name, 'unicode')
self.failUnless('lower' in infered._proxied.locals)
def test_descriptor_are_callable(self):
data = '''
class A:
statm = staticmethod(open)
clsm = classmethod('whatever')
'''
astng = builder.string_build(data, __name__, __file__)
statm = astng['A'].igetattr('statm').next()
self.failUnless(statm.callable())
clsm = astng['A'].igetattr('clsm').next()
self.failUnless(clsm.callable())
def test_bt_ancestor_crash(self):
data = '''
class Warning(Warning):
pass
'''
astng = builder.string_build(data, __name__, __file__)
w = astng['Warning']
ancestors = w.ancestors()
ancestor = ancestors.next()
self.failUnlessEqual(ancestor.name, 'Warning')
self.failUnlessEqual(ancestor.root().name, 'exceptions')
ancestor = ancestors.next()
self.failUnlessEqual(ancestor.name, 'Exception')
self.failUnlessEqual(ancestor.root().name, 'exceptions')
if sys.version_info >= (2, 5):
ancestor = ancestors.next()
self.failUnlessEqual(ancestor.name, 'BaseException')
self.failUnlessEqual(ancestor.root().name, 'exceptions')
ancestor = ancestors.next()
self.failUnlessEqual(ancestor.name, 'object')
self.failUnlessEqual(ancestor.root().name, '__builtin__')
self.failUnlessRaises(StopIteration, ancestors.next)
def test_qqch(self):
data = '''
from logilab.common.modutils import load_module_from_name
xxx = load_module_from_name('__pkginfo__')
'''
astng = builder.string_build(data, __name__, __file__)
xxx = astng['xxx']
infered = list(xxx.infer())
self.failUnlessEqual([n.__class__ for n in infered],
[nodes.Const, YES.__class__])
def test_method_argument(self):
data = '''
class ErudiEntitySchema:
"""a entity has a type, a set of subject and or object relations"""
def __init__(self, e_type, **kwargs):
kwargs['e_type'] = e_type.capitalize().encode()
def meth(self, e_type, *args, **kwargs):
kwargs['e_type'] = e_type.capitalize().encode()
print args
'''
astng = builder.string_build(data, __name__, __file__)
arg = get_name_node(astng['ErudiEntitySchema']['__init__'], 'e_type')
self.failUnlessEqual([n.__class__ for n in arg.infer()],
[YES.__class__])
arg = get_name_node(astng['ErudiEntitySchema']['__init__'], 'kwargs')
self.failUnlessEqual([n.__class__ for n in arg.infer()],
[nodes.Dict])
arg = get_name_node(astng['ErudiEntitySchema']['meth'], 'e_type')
self.failUnlessEqual([n.__class__ for n in arg.infer()],
[YES.__class__])
arg = get_name_node(astng['ErudiEntitySchema']['meth'], 'args')
self.failUnlessEqual([n.__class__ for n in arg.infer()],
[nodes.Tuple])
arg = get_name_node(astng['ErudiEntitySchema']['meth'], 'kwargs')
self.failUnlessEqual([n.__class__ for n in arg.infer()],
[nodes.Dict])
def test_tuple_then_list(self):
data = '''
def test_view(rql, vid, tags=()):
tags = list(tags)
tags.append(vid)
'''
astng = builder.string_build(data, __name__, __file__)
name = get_name_node(astng['test_view'], 'tags', -1)
it = name.infer()
tags = it.next()
self.failUnlessEqual(tags.__class__, Instance)
self.failUnlessEqual(tags._proxied.name, 'list')
self.failUnlessRaises(StopIteration, it.next)
def test_mulassign_inference(self):
data = '''
def first_word(line):
"""Return the first word of a line"""
return line.split()[0]
def last_word(line):
"""Return last word of a line"""
return line.split()[-1]
def process_line(word_pos):
"""Silly function: returns (ok, callable) based on argument.
For test purpose only.
"""
if word_pos > 0:
return (True, first_word)
elif word_pos < 0:
return (True, last_word)
else:
return (False, None)
if __name__ == '__main__':
line_number = 0
for a_line in file('test_callable.py'):
tupletest = process_line(line_number)
(ok, fct) = process_line(line_number)
if ok:
fct(a_line)
'''
astng = builder.string_build(data, __name__, __file__)
self.failUnlessEqual(len(list(astng['process_line'].infer_call_result(None))),
3)
self.failUnlessEqual(len(list(astng['tupletest'].infer())),
3)
self.failUnlessEqual([str(infered)
for infered in astng['fct'].infer()],
['Function(first_word)', 'Function(last_word)', 'Const(None)'])
def test_float_complex_ambiguity(self):
data = '''
def no_conjugate_member(magic_flag):
"""should not raise E1101 on something.conjugate"""
if magic_flag:
something = 1.0
else:
something = 1.0j
if isinstance(something, float):
return something
return something.conjugate()
'''
astng = builder.string_build(data, __name__, __file__)
self.failUnlessEqual([i.value for i in astng['no_conjugate_member'].ilookup('something')],
[1.0, 1.0j])
self.failUnlessEqual([i.value for i in get_name_node(astng, 'something', -1).infer()],
[1.0, 1.0j])
def test_simple_subscript(self):
data = '''
a = [1, 2, 3][0]
b = (1, 2, 3)[1]
c = (1, 2, 3)[-1]
d = a + b + c
print d
'''
astng = builder.string_build(data, __name__, __file__)
self.failUnlessEqual([i.value for i in get_name_node(astng, 'a', -1).infer()],
[1])
self.failUnlessEqual([i.value for i in get_name_node(astng, 'b', -1).infer()],
[2])
self.failUnlessEqual([i.value for i in get_name_node(astng, 'c', -1).infer()],
[3])
# kill me
#self.failUnlessEqual([i.value for i in get_name_node(astng, 'd', -1).infer()],
# [6])
def test_simple_for(self):
data = '''
for a in [1, 2, 3]:
print a
for b,c in [(1,2), (3,4)]:
print b
print c
print [(d,e) for e,d in ([1,2], [3,4])]
'''
astng = builder.string_build(data, __name__, __file__)
self.failUnlessEqual([i.value for i in get_name_node(astng, 'a', -1).infer()],
[1, 2, 3])
self.failUnlessEqual([i.value for i in get_name_node(astng, 'b', -1).infer()],
[1, 3])
self.failUnlessEqual([i.value for i in get_name_node(astng, 'c', -1).infer()],
[2, 4])
self.failUnlessEqual([i.value for i in get_name_node(astng, 'd', -1).infer()],
[2, 4])
self.failUnlessEqual([i.value for i in get_name_node(astng, 'e', -1).infer()],
[1, 3])
def test_simple_for_genexpr(self):
if sys.version_info < (2, 4):
return
data = '''
print ((d,e) for e,d in ([1,2], [3,4]))
'''
astng = builder.string_build(data, __name__, __file__)
self.failUnlessEqual([i.value for i in get_name_node(astng, 'd', -1).infer()],
[2, 4])
self.failUnlessEqual([i.value for i in get_name_node(astng, 'e', -1).infer()],
[1, 3])
def test_builtin_help(self):
data = '''
help()
'''
astng = builder.string_build(data, __name__, __file__)
node = get_name_node(astng, 'help', -1)
infered = list(node.infer())
self.failUnlessEqual(len(infered), 1)
self.assertIsInstance(infered[0], Instance)
self.failUnlessEqual(str(infered[0]),
'Instance of site._Helper')
def test_builtin_open(self):
data = '''
open("toto.txt")
'''
astng = builder.string_build(data, __name__, __file__)
node = get_name_node(astng, 'open', -1)
infered = list(node.infer())
self.failUnlessEqual(len(infered), 1)
self.assertIsInstance(infered[0], nodes.Class)
self.failUnlessEqual(infered[0].name, 'file')
def test_callfunc_context_inference(self):
data = '''
def mirror(arg=None):
return arg
un = mirror(1)
'''
astng = builder.string_build(data, __name__, __file__)
infered = list(astng.igetattr('un'))
self.failUnlessEqual(len(infered), 1)
self.assertIsInstance(infered[0], nodes.Const)
self.failUnlessEqual(infered[0].value, 1)
def test_callfunc_context_inference_lambda(self):
data = '''
mirror = lambda x=None: x
un = mirror(1)
'''
astng = builder.string_build(data, __name__, __file__)
infered = list(astng.igetattr('un'))
self.failUnlessEqual(len(infered), 1)
self.assertIsInstance(infered[0], nodes.Const)
self.failUnlessEqual(infered[0].value, 1)
def test_factory_method(self):
data = '''
class Super(object):
@classmethod
def instance(cls):
return cls()
class Sub(Super):
def method(self):
print 'method called'
sub = Sub.instance()
'''
astng = builder.string_build(data, __name__, __file__)
infered = list(astng.igetattr('sub'))
self.failUnlessEqual(len(infered), 1)
self.assertIsInstance(infered[0], Instance)
self.failUnlessEqual(infered[0]._proxied.name, 'Sub')
def test_base_operator(self):
data = '''
a = "*" * 80
b = 1 / 2.
c = b - 1
d = [[]]*3
class myarray:
def __init__(self, array):
self.array = array
def __mul__(self, x):
return myarray([2,4,6])
def astype(self):
return "ASTYPE"
def randint(maximum):
if maximum is not None:
return myarray([1,2,3]) * 2
else:
return int(5)
x = randint(1)
'''
astng = builder.string_build(data, __name__, __file__)
# a
infered = list(astng.igetattr('a'))
self.failUnlessEqual(len(infered), 1)
value = infered[0]
self.assertIsInstance(value, nodes.Const)
self.failUnlessEqual(value._proxied.name, 'str')
# hey...
self.failUnlessEqual(value.value, '********************************************************************************')
# b
infered = list(astng.igetattr('b'))
self.failUnlessEqual(len(infered), 1)
value = infered[0]
self.assertIsInstance(value, nodes.Const)
self.failUnlessEqual(value._proxied.name, 'float')
self.failUnlessEqual(value.value, 1/2.)
# c
infered = list(astng.igetattr('c'))
self.failUnlessEqual(len(infered), 1)
value = infered[0]
self.assertIsInstance(value, nodes.Const)
self.failUnlessEqual(value._proxied.name, 'float')
self.failUnlessEqual(value.value, 1/2.-1)
# d
infered = list(astng.igetattr('d'))
self.failUnlessEqual(len(infered), 1, infered)
value = infered[0]
self.assertIsInstance(value, nodes.List)
# x
infered = list(astng.igetattr('x'))
self.failUnlessEqual(len(infered), 2)
value = [str(v) for v in infered]
self.assertEquals(value, ['Instance of __main__.Yyarray',
'Instance of __builtin__.int'])
if __name__ == '__main__':
from logilab.common.testlib import unittest_main
unittest_main()
syntax highlighted by Code2HTML, v. 0.9.1