summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEevee (Alex Munroe) <amunroe@yelp.com>2014-07-31 16:32:21 -0700
committerEevee (Alex Munroe) <amunroe@yelp.com>2014-07-31 16:32:21 -0700
commit3a090e2819c85abacae5dd244733408cb110e427 (patch)
tree22afd25a9e0b62afb2c456c8f63fd2b69d597f02
parent58e5e9774ba6a5cb378f2e30eb026489d66922fd (diff)
parentbf919348a823a573ebbb7c435626172ba21b7c3c (diff)
downloadastroid-git-3a090e2819c85abacae5dd244733408cb110e427.tar.gz
merged with default
-rw-r--r--.hgtags1
-rw-r--r--ChangeLog32
-rw-r--r--__pkginfo__.py7
-rw-r--r--as_string.py83
-rw-r--r--bases.py11
-rw-r--r--brain/py2stdlib.py183
-rw-r--r--builder.py23
-rw-r--r--doc/release.txt23
-rw-r--r--inference.py17
-rw-r--r--inspector.py56
-rw-r--r--manager.py26
-rw-r--r--mixins.py8
-rw-r--r--modutils.py635
-rw-r--r--node_classes.py60
-rw-r--r--protocols.py12
-rw-r--r--raw_building.py12
-rw-r--r--rebuilder.py59
-rw-r--r--scoped_nodes.py156
-rw-r--r--test/data/find_test/__init__.py0
-rw-r--r--test/data/find_test/module.py0
-rw-r--r--test/data/find_test/module2.py0
-rw-r--r--test/data/find_test/noendingnewline.py0
-rw-r--r--test/data/find_test/nonregr.py0
-rw-r--r--test/data/lmfp/__init__.py2
-rw-r--r--test/data/lmfp/foo.py6
-rw-r--r--test/data/module.py2
-rw-r--r--test/data/module1abs/__init__.py4
-rw-r--r--test/data/module1abs/core.py1
-rw-r--r--test/unittest_inference.py22
-rw-r--r--test/unittest_manager.py32
-rw-r--r--test/unittest_modutils.py282
-rw-r--r--test/unittest_nodes.py10
-rw-r--r--test/unittest_python3.py52
-rw-r--r--test/unittest_regrtest.py27
-rw-r--r--test/unittest_scoped_nodes.py113
-rw-r--r--test_utils.py6
-rw-r--r--utils.py22
37 files changed, 1727 insertions, 258 deletions
diff --git a/.hgtags b/.hgtags
index 8b19c6ac..c145d4fb 100644
--- a/.hgtags
+++ b/.hgtags
@@ -48,3 +48,4 @@ aeb398869afc7f395ca310272144fb7715f53be7 astroid-debian-version-1.0.0-1
f67f24131b3a19f7a1f8ac59b7d18ad95a9c1d5b astroid-version-1.0.1
0f3825899d581064c8dae86ade4afb44a7feb72f astroid-debian-version-1.0.1-1
e003574ae51bff3a42fda97b2b7cc0e07b281a73 astroid-1.1
+59b1c5e1bd8dd43cc912b8d80ed817ac229a6bfe astroid-1.2
diff --git a/ChangeLog b/ChangeLog
index f7e09532..2fff5b84 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -2,13 +2,45 @@ Change log for the astroid package (used to be astng)
=====================================================
--
+ * Fix a crash occurred when inferring decorator call chain.
+ Closes issue #42.
+
+ * Set the parent of vararg and kwarg nodes when inferring them.
+ Closes issue #43.
+
+2014-07-25 -- 1.2.0
+
+ * Function nodes can detect decorator call chain and see if they are
+ decorated with builtin descriptors (`classmethod` and `staticmethod`).
+
+ * infer_call_result called on a subtype of the builtin type will now
+ return a new `Class` rather than an `Instance`.
+
* `Class.metaclass()` now handles module-level __metaclass__ declaration
on python 2, and no longer looks at the __metaclass__ class attribute on
python 3.
+
* Function nodes can detect if they are decorated with subclasses
of builtin descriptors when determining their type
(`classmethod` and `staticmethod`).
+ * Add `slots` method to `Class` nodes, for retrieving
+ the list of valid slots it defines.
+
+ * Expose function annotation to astroid: `Arguments` node
+ exposes 'varargannotation', 'kwargannotation' and 'annotations'
+ attributes, while `Function` node has the 'returns' attribute.
+
+ * Backported most of the logilab.common.modutils module there, as
+ most things there are for pylint/astroid only and we want to be
+ able to fix them without requiring a new logilab.common release
+
+ * Fix names grabed using wildcard import in "absolute import mode"
+ (ie with absolute_import activated from the __future__ or with
+ python 3). Fix pylint issue #58.
+
+ * Add support in pylint-brain for understanding enum classes.
+
2014-04-30 -- 1.1.1
* `Class.metaclass()` looks in ancestors when the current class
does not define explicitly a metaclass.
diff --git a/__pkginfo__.py b/__pkginfo__.py
index 85398ff1..46df5970 100644
--- a/__pkginfo__.py
+++ b/__pkginfo__.py
@@ -21,7 +21,7 @@ distname = 'astroid'
modname = 'astroid'
-numversion = (1, 1, 1)
+numversion = (1, 2, 0)
version = '.'.join([str(num) for num in numversion])
install_requires = ['logilab-common >= 0.60.0']
@@ -38,11 +38,12 @@ description = "rebuild a new abstract syntax tree from Python's ast"
from os.path import join
include_dirs = ['brain',
join('test', 'regrtest_data'),
- join('test', 'data'), join('test', 'data2')]
+ join('test', 'data'), join('test', 'data2')
+ ]
classifiers = ["Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: Software Development :: Quality Assurance",
"Programming Language :: Python",
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 3",
- ]
+ ]
diff --git a/as_string.py b/as_string.py
index ace1c4e3..f19713d9 100644
--- a/as_string.py
+++ b/as_string.py
@@ -44,29 +44,29 @@ def _repr_tree(node, result, indent='', _done=None, ids=False):
if not hasattr(node, '_astroid_fields'): # not a astroid node
return
if node in _done:
- result.append( indent + 'loop in tree: %s' % node )
+ result.append(indent + 'loop in tree: %s' % node)
return
_done.add(node)
node_str = str(node)
if ids:
node_str += ' . \t%x' % id(node)
- result.append( indent + node_str )
+ result.append(indent + node_str)
indent += INDENT
for field in node._astroid_fields:
value = getattr(node, field)
- if isinstance(value, (list, tuple) ):
- result.append( indent + field + " = [" )
+ if isinstance(value, (list, tuple)):
+ result.append(indent + field + " = [")
for child in value:
- if isinstance(child, (list, tuple) ):
+ if isinstance(child, (list, tuple)):
# special case for Dict # FIXME
_repr_tree(child[0], result, indent, _done, ids)
_repr_tree(child[1], result, indent, _done, ids)
result.append(indent + ',')
else:
_repr_tree(child, result, indent, _done, ids)
- result.append( indent + "]" )
+ result.append(indent + "]")
else:
- result.append( indent + field + " = " )
+ result.append(indent + field + " = ")
_repr_tree(value, result, indent, _done, ids)
@@ -97,7 +97,7 @@ class AsStringVisitor(object):
"""return an astroid.Assert node as string"""
if node.fail:
return 'assert %s, %s' % (node.test.accept(self),
- node.fail.accept(self))
+ node.fail.accept(self))
return 'assert %s' % node.test.accept(self)
def visit_assname(self, node):
@@ -124,7 +124,7 @@ class AsStringVisitor(object):
def visit_boolop(self, node):
"""return an astroid.BoolOp node as string"""
return (' %s ' % node.op).join(['(%s)' % n.accept(self)
- for n in node.values])
+ for n in node.values])
def visit_break(self, node):
"""return an astroid.Break node as string"""
@@ -135,15 +135,15 @@ class AsStringVisitor(object):
expr_str = node.func.accept(self)
args = [arg.accept(self) for arg in node.args]
if node.starargs:
- args.append( '*' + node.starargs.accept(self))
+ args.append('*' + node.starargs.accept(self))
if node.kwargs:
- args.append( '**' + node.kwargs.accept(self))
+ args.append('**' + node.kwargs.accept(self))
return '%s(%s)' % (expr_str, ', '.join(args))
def visit_class(self, node):
"""return an astroid.Class node as string"""
decorate = node.decorators and node.decorators.accept(self) or ''
- bases = ', '.join([n.accept(self) for n in node.bases])
+ bases = ', '.join([n.accept(self) for n in node.bases])
if sys.version_info[0] == 2:
bases = bases and '(%s)' % bases or ''
else:
@@ -157,7 +157,7 @@ class AsStringVisitor(object):
bases = bases and '(%s)' % bases or ''
docs = node.doc and '\n%s"""%s"""' % (INDENT, node.doc) or ''
return '\n\n%sclass %s%s:%s\n%s\n' % (decorate, node.name, bases, docs,
- self._stmt_list( node.body))
+ self._stmt_list(node.body))
def visit_compare(self, node):
"""return an astroid.Compare node as string"""
@@ -167,9 +167,9 @@ class AsStringVisitor(object):
def visit_comprehension(self, node):
"""return an astroid.Comprehension node as string"""
- ifs = ''.join([ ' if %s' % n.accept(self) for n in node.ifs])
+ ifs = ''.join([' if %s' % n.accept(self) for n in node.ifs])
return 'for %s in %s%s' % (node.target.accept(self),
- node.iter.accept(self), ifs )
+ node.iter.accept(self), ifs)
def visit_const(self, node):
"""return an astroid.Const node as string"""
@@ -182,7 +182,7 @@ class AsStringVisitor(object):
def visit_delete(self, node): # XXX check if correct
"""return an astroid.Delete node as string"""
return 'del %s' % ', '.join([child.accept(self)
- for child in node.targets])
+ for child in node.targets])
def visit_delattr(self, node):
"""return an astroid.DelAttr node as string"""
@@ -199,12 +199,13 @@ class AsStringVisitor(object):
def visit_dict(self, node):
"""return an astroid.Dict node as string"""
return '{%s}' % ', '.join(['%s: %s' % (key.accept(self),
- value.accept(self)) for key, value in node.items])
+ value.accept(self))
+ for key, value in node.items])
def visit_dictcomp(self, node):
"""return an astroid.DictComp node as string"""
return '{%s: %s %s}' % (node.key.accept(self), node.value.accept(self),
- ' '.join([n.accept(self) for n in node.generators]))
+ ' '.join([n.accept(self) for n in node.generators]))
def visit_discard(self, node):
"""return an astroid.Discard node as string"""
@@ -218,7 +219,7 @@ class AsStringVisitor(object):
if node.type:
if node.name:
excs = 'except %s, %s' % (node.type.accept(self),
- node.name.accept(self))
+ node.name.accept(self))
else:
excs = 'except %s' % node.type.accept(self)
else:
@@ -246,13 +247,13 @@ class AsStringVisitor(object):
def visit_extslice(self, node):
"""return an astroid.ExtSlice node as string"""
- return ','.join( [dim.accept(self) for dim in node.dims] )
+ return ','.join([dim.accept(self) for dim in node.dims])
def visit_for(self, node):
"""return an astroid.For node as string"""
fors = 'for %s in %s:\n%s' % (node.target.accept(self),
- node.iter.accept(self),
- self._stmt_list( node.body))
+ node.iter.accept(self),
+ self._stmt_list(node.body))
if node.orelse:
fors = '%s\nelse:\n%s' % (fors, self._stmt_list(node.orelse))
return fors
@@ -267,12 +268,12 @@ class AsStringVisitor(object):
decorate = node.decorators and node.decorators.accept(self) or ''
docs = node.doc and '\n%s"""%s"""' % (INDENT, node.doc) or ''
return '\n%sdef %s(%s):%s\n%s' % (decorate, node.name, node.args.accept(self),
- docs, self._stmt_list(node.body))
+ docs, self._stmt_list(node.body))
def visit_genexpr(self, node):
"""return an astroid.GenExpr node as string"""
- return '(%s %s)' % (node.elt.accept(self), ' '.join([n.accept(self)
- for n in node.generators]))
+ return '(%s %s)' % (node.elt.accept(self),
+ ' '.join([n.accept(self) for n in node.generators]))
def visit_getattr(self, node):
"""return an astroid.Getattr node as string"""
@@ -292,7 +293,8 @@ class AsStringVisitor(object):
def visit_ifexp(self, node):
"""return an astroid.IfExp node as string"""
return '%s if %s else %s' % (node.body.accept(self),
- node.test.accept(self), node.orelse.accept(self))
+ node.test.accept(self),
+ node.orelse.accept(self))
def visit_import(self, node):
"""return an astroid.Import node as string"""
@@ -304,7 +306,8 @@ class AsStringVisitor(object):
def visit_lambda(self, node):
"""return an astroid.Lambda node as string"""
- return 'lambda %s: %s' % (node.args.accept(self), node.body.accept(self))
+ return 'lambda %s: %s' % (node.args.accept(self),
+ node.body.accept(self))
def visit_list(self, node):
"""return an astroid.List node as string"""
@@ -312,8 +315,8 @@ class AsStringVisitor(object):
def visit_listcomp(self, node):
"""return an astroid.ListComp node as string"""
- return '[%s %s]' % (node.elt.accept(self), ' '.join([n.accept(self)
- for n in node.generators]))
+ return '[%s %s]' % (node.elt.accept(self),
+ ' '.join([n.accept(self) for n in node.generators]))
def visit_module(self, node):
"""return an astroid.Module node as string"""
@@ -343,10 +346,10 @@ class AsStringVisitor(object):
if node.inst:
if node.tback:
return 'raise %s, %s, %s' % (node.exc.accept(self),
- node.inst.accept(self),
- node.tback.accept(self))
+ node.inst.accept(self),
+ node.tback.accept(self))
return 'raise %s, %s' % (node.exc.accept(self),
- node.inst.accept(self))
+ node.inst.accept(self))
return 'raise %s' % node.exc.accept(self)
return 'raise'
@@ -367,8 +370,8 @@ class AsStringVisitor(object):
def visit_setcomp(self, node):
"""return an astroid.SetComp node as string"""
- return '{%s %s}' % (node.elt.accept(self), ' '.join([n.accept(self)
- for n in node.generators]))
+ return '{%s %s}' % (node.elt.accept(self),
+ ' '.join([n.accept(self) for n in node.generators]))
def visit_slice(self, node):
"""return a astroid.Slice node as string"""
@@ -385,7 +388,7 @@ class AsStringVisitor(object):
def visit_tryexcept(self, node):
"""return an astroid.TryExcept node as string"""
- trys = ['try:\n%s' % self._stmt_list( node.body)]
+ trys = ['try:\n%s' % self._stmt_list(node.body)]
for handler in node.handlers:
trys.append(handler.accept(self))
if node.orelse:
@@ -394,13 +397,13 @@ class AsStringVisitor(object):
def visit_tryfinally(self, node):
"""return an astroid.TryFinally node as string"""
- return 'try:\n%s\nfinally:\n%s' % (self._stmt_list( node.body),
- self._stmt_list(node.finalbody))
+ return 'try:\n%s\nfinally:\n%s' % (self._stmt_list(node.body),
+ self._stmt_list(node.finalbody))
def visit_tuple(self, node):
"""return an astroid.Tuple node as string"""
if len(node.elts) == 1:
- return '(%s, )' % node.elts[0].accept(self)
+ return '(%s, )' % node.elts[0].accept(self)
return '(%s)' % ', '.join([child.accept(self) for child in node.elts])
def visit_unaryop(self, node):
@@ -424,7 +427,7 @@ class AsStringVisitor(object):
items = ', '.join(('(%s)' % expr.accept(self)) +
(vars and ' as (%s)' % (vars.accept(self)) or '')
for expr, vars in node.items)
- return 'with %s:\n%s' % (items, self._stmt_list( node.body))
+ return 'with %s:\n%s' % (items, self._stmt_list(node.body))
def visit_yield(self, node):
"""yield an ast.Yield node as string"""
@@ -443,7 +446,7 @@ class AsStringVisitor3k(AsStringVisitor):
if node.type:
if node.name:
excs = 'except %s as %s' % (node.type.accept(self),
- node.name.accept(self))
+ node.name.accept(self))
else:
excs = 'except %s' % node.type.accept(self)
else:
diff --git a/bases.py b/bases.py
index ac3414ba..496c8876 100644
--- a/bases.py
+++ b/bases.py
@@ -293,7 +293,7 @@ class UnboundMethod(Proxy):
class BoundMethod(UnboundMethod):
"""a special node representing a method bound to an instance"""
- def __init__(self, proxy, bound):
+ def __init__(self, proxy, bound):
UnboundMethod.__init__(self, proxy)
self.bound = bound
@@ -424,14 +424,13 @@ class NodeNG(object):
def __repr__(self):
return '<%s(%s) l.%s [%s] at 0x%x>' % (self.__class__.__name__,
- self._repr_name(),
- self.fromlineno,
- self.root().name,
- id(self))
+ self._repr_name(),
+ self.fromlineno,
+ self.root().name,
+ id(self))
def accept(self, visitor):
- klass = self.__class__.__name__
func = getattr(visitor, "visit_" + self.__class__.__name__.lower())
return func(self)
diff --git a/brain/py2stdlib.py b/brain/py2stdlib.py
index f7311ef7..88cde92d 100644
--- a/brain/py2stdlib.py
+++ b/brain/py2stdlib.py
@@ -6,14 +6,58 @@ Currently help understanding of :
"""
import sys
+from textwrap import dedent
-from astroid import MANAGER, UseInferenceDefault, inference_tip, YES
+from astroid import MANAGER, AsStringRegexpPredicate, UseInferenceDefault, inference_tip, YES
from astroid import exceptions
from astroid import nodes
from astroid.builder import AstroidBuilder
MODULE_TRANSFORMS = {}
PY3K = sys.version_info > (3, 0)
+PY33 = sys.version_info >= (3, 3)
+
+# general function
+
+def infer_func_form(node, base_type, context=None):
+ """Specific inference function for namedtuple or Python 3 enum. """
+ def infer_first(node):
+ try:
+ value = node.infer(context=context).next()
+ if value is YES:
+ raise UseInferenceDefault()
+ else:
+ return value
+ except StopIteration:
+ raise InferenceError()
+
+ # node is a CallFunc node, class name as first argument and generated class
+ # attributes as second argument
+ if len(node.args) != 2:
+ # something weird here, go back to class implementation
+ raise UseInferenceDefault()
+ # namedtuple or enums list of attributes can be a list of strings or a
+ # whitespace-separate string
+ try:
+ name = infer_first(node.args[0]).value
+ names = infer_first(node.args[1])
+ try:
+ attributes = names.value.replace(',', ' ').split()
+ except AttributeError:
+ attributes = [infer_first(const).value for const in names.elts]
+ except (AttributeError, exceptions.InferenceError):
+ raise UseInferenceDefault()
+ # we want to return a Class node instance with proper attributes set
+ class_node = nodes.Class(name, 'docstring')
+ class_node.parent = node.parent
+ # set base class=tuple
+ class_node.bases.append(base_type)
+ # XXX add __init__(*attributes) method
+ for attr in attributes:
+ fake_node = nodes.EmptyNode()
+ fake_node.parent = class_node
+ class_node.instance_attrs[attr] = [fake_node]
+ return class_node, name, attributes
# module specific transformation functions #####################################
@@ -58,7 +102,7 @@ class defaultdict(dict):
class deque(object):
maxlen = 0
- def __init__(iterable=None, maxlen=None): pass
+ def __init__(self, iterable=None, maxlen=None): pass
def append(self, x): pass
def appendleft(self, x): pass
def clear(self): pass
@@ -148,24 +192,40 @@ class ParseResult(object):
def subprocess_transform(module):
if PY3K:
communicate = (bytes('string', 'ascii'), bytes('string', 'ascii'))
+ init = """
+ def __init__(self, args, bufsize=0, executable=None,
+ stdin=None, stdout=None, stderr=None,
+ preexec_fn=None, close_fds=False, shell=False,
+ cwd=None, env=None, universal_newlines=False,
+ startupinfo=None, creationflags=0, restore_signals=True,
+ start_new_session=False, pass_fds=()):
+ pass
+ """
else:
communicate = ('string', 'string')
- fake = AstroidBuilder(MANAGER).string_build('''
-
-class Popen(object):
- returncode = pid = 0
- stdin = stdout = stderr = file()
-
+ init = """
def __init__(self, args, bufsize=0, executable=None,
stdin=None, stdout=None, stderr=None,
preexec_fn=None, close_fds=False, shell=False,
cwd=None, env=None, universal_newlines=False,
startupinfo=None, creationflags=0):
pass
+ """
+ if PY33:
+ wait_signature = 'def wait(self, timeout=None)'
+ else:
+ wait_signature = 'def wait(self)'
+ fake = AstroidBuilder(MANAGER).string_build('''
+
+class Popen(object):
+ returncode = pid = 0
+ stdin = stdout = stderr = file()
+
+ %(init)s
def communicate(self, input=None):
- return %r
- def wait(self):
+ return %(communicate)r
+ %(wait_signature)s:
return self.returncode
def poll(self):
return self.returncode
@@ -175,7 +235,9 @@ class Popen(object):
pass
def kill(self):
pass
- ''' % (communicate, ))
+ ''' % {'init': init,
+ 'communicate': communicate,
+ 'wait_signature': wait_signature})
for func_name, func in fake.locals.items():
module.locals[func_name] = func
@@ -190,45 +252,18 @@ MODULE_TRANSFORMS['subprocess'] = subprocess_transform
# namedtuple support ###########################################################
+def looks_like_namedtuple(node):
+ func = node.func
+ if type(func) is nodes.Getattr:
+ return func.attrname == 'namedtuple'
+ if type(func) is nodes.Name:
+ return func.name == 'namedtuple'
+ return False
+
def infer_named_tuple(node, context=None):
"""Specific inference function for namedtuple CallFunc node"""
- def infer_first(node):
- try:
- value = node.infer().next()
- if value is YES:
- raise UseInferenceDefault()
- else:
- return value
- except StopIteration:
- raise InferenceError()
-
- # node is a CallFunc node, class name as first argument and generated class
- # attributes as second argument
- if len(node.args) != 2:
- # something weird here, go back to class implementation
- raise UseInferenceDefault()
- # namedtuple list of attributes can be a list of strings or a
- # whitespace-separate string
- try:
- name = infer_first(node.args[0]).value
- names = infer_first(node.args[1])
- try:
- attributes = names.value.split()
- except AttributeError:
- attributes = [infer_first(const).value for const in names.elts]
- except (AttributeError, exceptions.InferenceError):
- raise UseInferenceDefault()
- # we want to return a Class node instance with proper attributes set
- class_node = nodes.Class(name, 'docstring')
- class_node.parent = node.parent
- # set base class=tuple
- class_node.bases.append(nodes.Tuple._proxied)
- # XXX add __init__(*attributes) method
- for attr in attributes:
- fake_node = nodes.EmptyNode()
- fake_node.parent = class_node
- class_node.instance_attrs[attr] = [fake_node]
-
+ class_node, name, attributes = infer_func_form(node, nodes.Tuple._proxied,
+ context=context)
fake = AstroidBuilder(MANAGER).string_build('''
class %(name)s(tuple):
def _asdict(self):
@@ -248,13 +283,51 @@ class %(name)s(tuple):
# we use UseInferenceDefault, we can't be a generator so return an iterator
return iter([class_node])
+def infer_enum(node, context=None):
+ """ Specific inference function for enum CallFunc node. """
+ enum_meta = nodes.Class("EnumMeta", 'docstring')
+ class_node = infer_func_form(node, enum_meta, context=context)[0]
+ return iter([class_node.instanciate_class()])
+
+def infer_enum_class(node, context=None):
+ """ Specific inference for enums. """
+ names = set(('Enum', 'IntEnum', 'enum.Enum', 'enum.IntEnum'))
+ for basename in node.basenames:
+ # TODO: doesn't handle subclasses yet.
+ if basename not in names:
+ continue
+ if node.root().name == 'enum':
+ # Skip if the class is directly from enum module.
+ break
+ for local, values in node.locals.items():
+ if any(not isinstance(value, nodes.AssName)
+ for value in values):
+ continue
+ parent = values[0].parent
+ real_value = parent.value
+ new_targets = []
+ for target in parent.targets:
+ # Replace all the assignments with our mocked class.
+ classdef = dedent('''
+ class %(name)s(object):
+ @property
+ def value(self):
+ return %(value)s
+ @property
+ def name(self):
+ return %(name)r
+ %(name)s = %(value)s
+ ''' % {'name': target.name,
+ 'value': real_value.as_string()})
+ fake = AstroidBuilder(MANAGER).string_build(classdef)[target.name]
+ fake.parent = target.parent
+ new_targets.append(fake.instanciate_class())
+ node.locals[local] = new_targets
+ break
+ return node
-def looks_like_namedtuple(node):
- func = node.func
- if type(func) is nodes.Getattr:
- return func.attrname == 'namedtuple'
- if type(func) is nodes.Name:
- return func.name == 'namedtuple'
- return False
MANAGER.register_transform(nodes.CallFunc, inference_tip(infer_named_tuple),
looks_like_namedtuple)
+MANAGER.register_transform(nodes.CallFunc, inference_tip(infer_enum),
+ AsStringRegexpPredicate('Enum', 'func'))
+MANAGER.register_transform(nodes.Class, infer_enum_class)
diff --git a/builder.py b/builder.py
index b6ceff82..692016a3 100644
--- a/builder.py
+++ b/builder.py
@@ -1,4 +1,4 @@
-# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of astroid.
@@ -20,19 +20,19 @@
The builder is not thread safe and can't be used to parse different sources
at the same time.
"""
+from __future__ import with_statement
__docformat__ = "restructuredtext en"
import sys
from os.path import splitext, basename, exists, abspath
-from logilab.common.modutils import modpath_from_file
-
from astroid.exceptions import AstroidBuildingException, InferenceError
from astroid.raw_building import InspectBuilder
from astroid.rebuilder import TreeRebuilder
from astroid.manager import AstroidManager
from astroid.bases import YES, Instance
+from astroid.modutils import modpath_from_file
from _ast import PyCF_ONLY_AST
def parse(string):
@@ -42,13 +42,12 @@ if sys.version_info >= (3, 0):
from tokenize import detect_encoding
def open_source_file(filename):
- byte_stream = open(filename, 'bU')
- encoding = detect_encoding(byte_stream.readline)[0]
- byte_stream.close()
- stream = open(filename, 'U', encoding=encoding)
+ with open(filename, 'rb') as byte_stream:
+ encoding = detect_encoding(byte_stream.readline)[0]
+ stream = open(filename, 'rU', encoding=encoding)
try:
data = stream.read()
- except UnicodeError, uex: # wrong encodingg
+ except UnicodeError: # wrong encodingg
# detect_encoding returns utf-8 if no encoding specified
msg = 'Wrong (%s) or no encoding specified' % encoding
raise AstroidBuildingException(msg)
@@ -57,7 +56,7 @@ if sys.version_info >= (3, 0):
else:
import re
- _ENCODING_RGX = re.compile("\s*#+.*coding[:=]\s*([-\w.]+)")
+ _ENCODING_RGX = re.compile(r"\s*#+.*coding[:=]\s*([-\w.]+)")
def _guess_encoding(string):
"""get encoding from a python file as string or return None if not found
@@ -116,7 +115,7 @@ class AstroidBuilder(InspectBuilder):
path is expected to be a python source file
"""
try:
- stream, encoding, data = open_source_file(path)
+ _, encoding, data = open_source_file(path)
except IOError, exc:
msg = 'Unable to load file %r (%s)' % (path, exc)
raise AstroidBuildingException(msg)
@@ -188,8 +187,8 @@ class AstroidBuilder(InspectBuilder):
for (name, asname) in node.names:
if name == '*':
try:
- imported = node.root().import_module(node.modname)
- except AstroidBuildingException:
+ imported = node.do_import_module()
+ except InferenceError:
continue
for name in imported.wildcard_import_names():
node.parent.set_local(name, node)
diff --git a/doc/release.txt b/doc/release.txt
new file mode 100644
index 00000000..5e4e0908
--- /dev/null
+++ b/doc/release.txt
@@ -0,0 +1,23 @@
+Release Process
+===============
+
+1. Preparatation
+ 1. Check if the dependencies of the package are correct
+ 2. Update the version number in __pkginfo__
+ 3. Put the version number and the release date into the changelog
+ 4. Submit your changes.
+
+2. Make sure the tests are passing on drone.io:
+ https://drone.io/bitbucket.org/logilab/astroid
+
+3. Add a new tag 'astroid-$VERSION'
+
+5. Publish all remaining changes to the Bitbucket repository:
+ https://bitbucket.org/logilab/astroid
+
+4. Run
+
+ $ python setup.py register sdist upload
+
+ to release a new version to PyPI.
+ \ No newline at end of file
diff --git a/inference.py b/inference.py
index 6fcb02d3..f9e45b66 100644
--- a/inference.py
+++ b/inference.py
@@ -25,16 +25,17 @@ from itertools import chain
from astroid import nodes
from astroid.manager import AstroidManager
-from astroid.exceptions import (AstroidError,
- InferenceError, NoDefault, NotFoundError, UnresolvableName)
-from astroid.bases import YES, Instance, InferenceContext, \
- _infer_stmts, path_wrapper, raise_if_nothing_infered
+from astroid.exceptions import (AstroidError, InferenceError, NoDefault,
+ NotFoundError, UnresolvableName)
+from astroid.bases import (YES, Instance, InferenceContext,
+ _infer_stmts, path_wrapper,
+ raise_if_nothing_infered)
from astroid.protocols import _arguments_infer_argname
MANAGER = AstroidManager()
-class CallContext:
+class CallContext(object):
"""when inferring a function call, this class is used to remember values
given as argument
"""
@@ -195,7 +196,7 @@ def infer_from(self, context=None, asname=True, lookupname=None):
raise InferenceError()
if asname:
lookupname = self.real_name(lookupname)
- module = self.do_import_module(self.modname)
+ module = self.do_import_module()
try:
return _infer_stmts(module.getattr(lookupname, ignore_locals=module is self.root()), context, lookupname=lookupname)
except NotFoundError:
@@ -303,7 +304,7 @@ BIN_OP_METHOD = {'+': '__add__',
'^': '__xor__',
'<<': '__lshift__',
'>>': '__rshift__',
- }
+ }
def _infer_binop(operator, operand1, operand2, context, failures=None):
if operand1 is YES:
@@ -376,7 +377,7 @@ def infer_empty_node(self, context=None):
else:
try:
for infered in MANAGER.infer_ast_from_something(self.object,
- context=context):
+ context=context):
yield infered
except AstroidError:
yield YES
diff --git a/inspector.py b/inspector.py
index db93a60d..1fc31926 100644
--- a/inspector.py
+++ b/inspector.py
@@ -25,25 +25,23 @@ __docformat__ = "restructuredtext en"
from os.path import dirname
-from logilab.common.modutils import get_module_part, is_relative, \
- is_standard_module
-
import astroid
from astroid.exceptions import InferenceError
from astroid.utils import LocalsVisitor
+from astroid.modutils import get_module_part, is_relative, is_standard_module
-class IdGeneratorMixIn:
+class IdGeneratorMixIn(object):
"""
Mixin adding the ability to generate integer uid
"""
def __init__(self, start_value=0):
self.id_count = start_value
-
+
def init_counter(self, start_value=0):
"""init the id counter
"""
self.id_count = start_value
-
+
def generate_id(self):
"""generate a new identifier
"""
@@ -54,14 +52,14 @@ class IdGeneratorMixIn:
class Linker(IdGeneratorMixIn, LocalsVisitor):
"""
walk on the project tree and resolve relationships.
-
+
According to options the following attributes may be added to visited nodes:
-
+
* uid,
a unique identifier for the node (on astroid.Project, astroid.Module,
astroid.Class and astroid.locals_type). Only if the linker has been instantiated
with tag=True parameter (False by default).
-
+
* Function
a mapping from locals names to their bounded value, which may be a
constant like a string or an integer, or an astroid node (on astroid.Module,
@@ -69,11 +67,11 @@ class Linker(IdGeneratorMixIn, LocalsVisitor):
* instance_attrs_type
as locals_type but for klass member attributes (only on astroid.Class)
-
+
* implements,
list of implemented interface _objects_ (only on astroid.Class nodes)
"""
-
+
def __init__(self, project, inherited_interfaces=0, tag=False):
IdGeneratorMixIn.__init__(self)
LocalsVisitor.__init__(self)
@@ -84,30 +82,30 @@ class Linker(IdGeneratorMixIn, LocalsVisitor):
# visited project
self.project = project
-
+
def visit_project(self, node):
"""visit an astroid.Project node
-
+
* optionally tag the node with a unique id
"""
if self.tag:
node.uid = self.generate_id()
for module in node.modules:
self.visit(module)
-
+
def visit_package(self, node):
"""visit an astroid.Package node
-
+
* optionally tag the node with a unique id
"""
if self.tag:
node.uid = self.generate_id()
for subelmt in node.values():
self.visit(subelmt)
-
+
def visit_module(self, node):
"""visit an astroid.Module node
-
+
* set the locals_type mapping
* set the depends mapping
* optionally tag the node with a unique id
@@ -118,10 +116,10 @@ class Linker(IdGeneratorMixIn, LocalsVisitor):
node.depends = []
if self.tag:
node.uid = self.generate_id()
-
+
def visit_class(self, node):
"""visit an astroid.Class node
-
+
* set the locals_type and instance_attrs_type mappings
* set the implements list and build it
* optionally tag the node with a unique id
@@ -149,7 +147,7 @@ class Linker(IdGeneratorMixIn, LocalsVisitor):
def visit_function(self, node):
"""visit an astroid.Function node
-
+
* set the locals_type mapping
* optionally tag the node with a unique id
"""
@@ -158,12 +156,12 @@ class Linker(IdGeneratorMixIn, LocalsVisitor):
node.locals_type = {}
if self.tag:
node.uid = self.generate_id()
-
+
link_project = visit_project
link_module = visit_module
link_class = visit_class
link_function = visit_function
-
+
def visit_assname(self, node):
"""visit an astroid.AssName node
@@ -178,7 +176,7 @@ class Linker(IdGeneratorMixIn, LocalsVisitor):
frame = node.frame()
else:
# the name has been defined as 'global' in the frame and belongs
- # there. Btw the frame is not yet visited as the name is in the
+ # there. Btw the frame is not yet visited as the name is in the
# root locals; the frame hence has no locals_type attribute
frame = node.root()
try:
@@ -209,21 +207,21 @@ class Linker(IdGeneratorMixIn, LocalsVisitor):
parent.instance_attrs_type[node.attrname] = values
except astroid.InferenceError:
pass
-
+
def visit_import(self, node):
"""visit an astroid.Import node
-
+
resolve module dependencies
"""
context_file = node.root().file
for name in node.names:
relative = is_relative(name[0], context_file)
self._imported_module(node, name[0], relative)
-
+
def visit_from(self, node):
"""visit an astroid.From node
-
+
resolve module dependencies
"""
basename = node.modname
@@ -246,7 +244,7 @@ class Linker(IdGeneratorMixIn, LocalsVisitor):
if fullname != basename:
self._imported_module(node, fullname, relative)
-
+
def compute_module(self, context_name, mod_path):
"""return true if the module should be added to dependencies"""
package_dir = dirname(self.project.path)
@@ -255,7 +253,7 @@ class Linker(IdGeneratorMixIn, LocalsVisitor):
elif is_standard_module(mod_path, (package_dir,)):
return 1
return 0
-
+
# protected methods ########################################################
def _imported_module(self, node, mod_path, relative):
diff --git a/manager.py b/manager.py
index 6e634b99..fde52e2b 100644
--- a/manager.py
+++ b/manager.py
@@ -26,12 +26,13 @@ import os
from os.path import dirname, join, isdir, exists
from warnings import warn
-from logilab.common.modutils import NoSourceFile, is_python_source, \
- file_from_modpath, load_module_from_name, modpath_from_file, \
- get_module_files, get_source_file, zipimport
from logilab.common.configuration import OptionsProviderMixIn
from astroid.exceptions import AstroidBuildingException
+from astroid.modutils import NoSourceFile, is_python_source, \
+ file_from_modpath, load_module_from_name, modpath_from_file, \
+ get_module_files, get_source_file, zipimport
+
def astroid_wrapper(func, modname):
"""wrapper to give to AstroidManager.project_from_files"""
@@ -74,7 +75,7 @@ class AstroidManager(OptionsProviderMixIn):
{'default': "No Name", 'type' : 'string', 'short': 'p',
'metavar' : '<project name>',
'help' : 'set the project name.'}),
- )
+ )
brain = {}
def __init__(self):
self.__dict__ = AstroidManager.brain
@@ -106,7 +107,7 @@ class AstroidManager(OptionsProviderMixIn):
elif fallback and modname:
return self.ast_from_module_name(modname)
raise AstroidBuildingException('unable to get astroid for file %s' %
- filepath)
+ filepath)
def ast_from_module_name(self, modname, context_file=None):
"""given a module name, return the astroid object"""
@@ -149,7 +150,7 @@ class AstroidManager(OptionsProviderMixIn):
importer = zipimport.zipimporter(eggpath + ext)
zmodname = resource.replace('/', '.')
if importer.is_package(resource):
- zmodname = zmodname + '.__init__'
+ zmodname = zmodname + '.__init__'
module = builder.string_build(importer.get_source(resource),
zmodname, filepath)
return module
@@ -272,11 +273,11 @@ class AstroidManager(OptionsProviderMixIn):
The transform function may return a value which is then used to
substitute the original node in the tree.
"""
- self.transforms.setdefault(node_class, []).append( (transform, predicate) )
+ self.transforms.setdefault(node_class, []).append((transform, predicate))
def unregister_transform(self, node_class, transform, predicate=None):
"""Unregister the given transform."""
- self.transforms[node_class].remove( (transform, predicate) )
+ self.transforms[node_class].remove((transform, predicate))
def transform(self, node):
"""Call matching transforms for the given node if any and return the
@@ -306,6 +307,15 @@ class AstroidManager(OptionsProviderMixIn):
"""Cache a module if no module with the same name is known yet."""
self.astroid_cache.setdefault(module.name, module)
+ def clear_cache(self):
+ self.astroid_cache.clear()
+ # force bootstrap again, else we may ends up with cache inconsistency
+ # between the manager and CONST_PROXY, making
+ # unittest_lookup.LookupTC.test_builtin_lookup fail depending on the
+ # test order
+ from astroid.raw_building import astroid_bootstrapping
+ astroid_bootstrapping()
+
class Project(object):
"""a project handle a set of modules / packages"""
diff --git a/mixins.py b/mixins.py
index 757c2af9..1b34c402 100644
--- a/mixins.py
+++ b/mixins.py
@@ -21,7 +21,7 @@
from logilab.common.decorators import cachedproperty
from astroid.exceptions import (AstroidBuildingException, InferenceError,
- NotFoundError)
+ NotFoundError)
class BlockRangeMixIn(object):
@@ -87,7 +87,7 @@ class FromImportMixIn(FilterStmtsMixin):
def _infer_name(self, frame, name):
return name
- def do_import_module(self, modname):
+ def do_import_module(self, modname=None):
"""return the ast for a module whose name is <modname> imported by <self>
"""
# handle special case where we are on a package node importing a module
@@ -96,6 +96,8 @@ class FromImportMixIn(FilterStmtsMixin):
# XXX: no more needed ?
mymodule = self.root()
level = getattr(self, 'level', None) # Import as no level
+ if modname is None:
+ modname = self.modname
# XXX we should investigate deeper if we really want to check
# importing itself: modname and mymodule.name be relative or absolute
if mymodule.relative_to_absolute_name(modname, level) == mymodule.name:
@@ -120,5 +122,3 @@ class FromImportMixIn(FilterStmtsMixin):
return name
raise NotFoundError(asname)
-
-
diff --git a/modutils.py b/modutils.py
new file mode 100644
index 00000000..1f4a4fc5
--- /dev/null
+++ b/modutils.py
@@ -0,0 +1,635 @@
+# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of astroid.
+#
+# astroid is free software: you can redistribute it and/or modify it under
+# the terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option) any
+# later version.
+#
+# astroid 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 Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with astroid. If not, see <http://www.gnu.org/licenses/>.
+"""Python modules manipulation utility functions.
+
+:type PY_SOURCE_EXTS: tuple(str)
+:var PY_SOURCE_EXTS: list of possible python source file extension
+
+:type STD_LIB_DIR: str
+:var STD_LIB_DIR: directory where standard modules are located
+
+:type BUILTIN_MODULES: dict
+:var BUILTIN_MODULES: dictionary with builtin module names has key
+"""
+from __future__ import with_statement
+
+__docformat__ = "restructuredtext en"
+
+import sys
+import os
+from os.path import splitext, join, abspath, isdir, dirname, exists
+from imp import find_module, load_module, C_BUILTIN, PY_COMPILED, PKG_DIRECTORY
+from distutils.sysconfig import get_python_lib
+from distutils.errors import DistutilsPlatformError
+
+try:
+ import zipimport
+except ImportError:
+ zipimport = None
+
+ZIPFILE = object()
+
+from logilab.common import _handle_blacklist
+
+# Notes about STD_LIB_DIR
+# Consider arch-specific installation for STD_LIB_DIR definition
+# :mod:`distutils.sysconfig` contains to much hardcoded values to rely on
+#
+# :see: `Problems with /usr/lib64 builds <http://bugs.python.org/issue1294959>`_
+# :see: `FHS <http://www.pathname.com/fhs/pub/fhs-2.3.html#LIBLTQUALGTALTERNATEFORMATESSENTIAL>`_
+if sys.platform.startswith('win'):
+ PY_SOURCE_EXTS = ('py', 'pyw')
+ PY_COMPILED_EXTS = ('dll', 'pyd')
+else:
+ PY_SOURCE_EXTS = ('py',)
+ PY_COMPILED_EXTS = ('so',)
+
+try:
+ STD_LIB_DIR = get_python_lib(standard_lib=1)
+# get_python_lib(standard_lib=1) is not available on pypy, set STD_LIB_DIR to
+# non-valid path, see https://bugs.pypy.org/issue1164
+except DistutilsPlatformError:
+ STD_LIB_DIR = '//'
+
+EXT_LIB_DIR = get_python_lib()
+
+BUILTIN_MODULES = dict(zip(sys.builtin_module_names,
+ [1]*len(sys.builtin_module_names)))
+
+
+class NoSourceFile(Exception):
+ """exception raised when we are not able to get a python
+ source file for a precompiled file
+ """
+
+
+def load_module_from_name(dotted_name, path=None, use_sys=1):
+ """Load a Python module from its name.
+
+ :type dotted_name: str
+ :param dotted_name: python name of a module or package
+
+ :type path: list or None
+ :param path:
+ optional list of path where the module or package should be
+ searched (use sys.path if nothing or None is given)
+
+ :type use_sys: bool
+ :param use_sys:
+ boolean indicating whether the sys.modules dictionary should be
+ used or not
+
+
+ :raise ImportError: if the module or package is not found
+
+ :rtype: module
+ :return: the loaded module
+ """
+ return load_module_from_modpath(dotted_name.split('.'), path, use_sys)
+
+
+def load_module_from_modpath(parts, path=None, use_sys=1):
+ """Load a python module from its splitted name.
+
+ :type parts: list(str) or tuple(str)
+ :param parts:
+ python name of a module or package splitted on '.'
+
+ :type path: list or None
+ :param path:
+ optional list of path where the module or package should be
+ searched (use sys.path if nothing or None is given)
+
+ :type use_sys: bool
+ :param use_sys:
+ boolean indicating whether the sys.modules dictionary should be used or not
+
+ :raise ImportError: if the module or package is not found
+
+ :rtype: module
+ :return: the loaded module
+ """
+ if use_sys:
+ try:
+ return sys.modules['.'.join(parts)]
+ except KeyError:
+ pass
+ modpath = []
+ prevmodule = None
+ for part in parts:
+ modpath.append(part)
+ curname = '.'.join(modpath)
+ module = None
+ if len(modpath) != len(parts):
+ # even with use_sys=False, should try to get outer packages from sys.modules
+ module = sys.modules.get(curname)
+ elif use_sys:
+ # because it may have been indirectly loaded through a parent
+ module = sys.modules.get(curname)
+ if module is None:
+ mp_file, mp_filename, mp_desc = find_module(part, path)
+ module = load_module(curname, mp_file, mp_filename, mp_desc)
+ if prevmodule:
+ setattr(prevmodule, part, module)
+ _file = getattr(module, '__file__', '')
+ if not _file and len(modpath) != len(parts):
+ raise ImportError('no module in %s' % '.'.join(parts[len(modpath):]))
+ path = [dirname(_file)]
+ prevmodule = module
+ return module
+
+
+def load_module_from_file(filepath, path=None, use_sys=1, extrapath=None):
+ """Load a Python module from it's path.
+
+ :type filepath: str
+ :param filepath: path to the python module or package
+
+ :type path: list or None
+ :param path:
+ optional list of path where the module or package should be
+ searched (use sys.path if nothing or None is given)
+
+ :type use_sys: bool
+ :param use_sys:
+ boolean indicating whether the sys.modules dictionary should be
+ used or not
+
+
+ :raise ImportError: if the module or package is not found
+
+ :rtype: module
+ :return: the loaded module
+ """
+ modpath = modpath_from_file(filepath, extrapath)
+ return load_module_from_modpath(modpath, path, use_sys)
+
+
+def _check_init(path, mod_path):
+ """check there are some __init__.py all along the way"""
+ for part in mod_path:
+ path = join(path, part)
+ if not _has_init(path):
+ return False
+ return True
+
+
+def modpath_from_file(filename, extrapath=None):
+ """given a file path return the corresponding splitted module's name
+ (i.e name of a module or package splitted on '.')
+
+ :type filename: str
+ :param filename: file's path for which we want the module's name
+
+ :type extrapath: dict
+ :param extrapath:
+ optional extra search path, with path as key and package name for the path
+ as value. This is usually useful to handle package splitted in multiple
+ directories using __path__ trick.
+
+
+ :raise ImportError:
+ if the corresponding module's name has not been found
+
+ :rtype: list(str)
+ :return: the corresponding splitted module's name
+ """
+ base = splitext(abspath(filename))[0]
+ if extrapath is not None:
+ for path_ in extrapath:
+ path = _abspath(path_)
+ if path and base[:len(path)] == path:
+ submodpath = [pkg for pkg in base[len(path):].split(os.sep)
+ if pkg]
+ if _check_init(path, submodpath[:-1]):
+ return extrapath[path_].split('.') + submodpath
+ for path in sys.path:
+ path = _abspath(path)
+ if path and base.startswith(path):
+ modpath = [pkg for pkg in base[len(path):].split(os.sep) if pkg]
+ if _check_init(path, modpath[:-1]):
+ return modpath
+ raise ImportError('Unable to find module for %s in %s' % (
+ filename, ', \n'.join(sys.path)))
+
+
+
+def file_from_modpath(modpath, path=None, context_file=None):
+ """given a mod path (i.e. splitted module / package name), return the
+ corresponding file, giving priority to source file over precompiled
+ file if it exists
+
+ :type modpath: list or tuple
+ :param modpath:
+ splitted module's name (i.e name of a module or package splitted
+ on '.')
+ (this means explicit relative imports that start with dots have
+ empty strings in this list!)
+
+ :type path: list or None
+ :param path:
+ optional list of path where the module or package should be
+ searched (use sys.path if nothing or None is given)
+
+ :type context_file: str or None
+ :param context_file:
+ context file to consider, necessary if the identifier has been
+ introduced using a relative import unresolvable in the actual
+ context (i.e. modutils)
+
+ :raise ImportError: if there is no such module in the directory
+
+ :rtype: str or None
+ :return:
+ the path to the module's file or None if it's an integrated
+ builtin module such as 'sys'
+ """
+ if context_file is not None:
+ context = dirname(context_file)
+ else:
+ context = context_file
+ if modpath[0] == 'xml':
+ # handle _xmlplus
+ try:
+ return _file_from_modpath(['_xmlplus'] + modpath[1:], path, context)
+ except ImportError:
+ return _file_from_modpath(modpath, path, context)
+ elif modpath == ['os', 'path']:
+ # FIXME: currently ignoring search_path...
+ return os.path.__file__
+ return _file_from_modpath(modpath, path, context)
+
+
+
+def get_module_part(dotted_name, context_file=None):
+ """given a dotted name return the module part of the name :
+
+ >>> get_module_part('logilab.common.modutils.get_module_part')
+ 'logilab.common.modutils'
+
+ :type dotted_name: str
+ :param dotted_name: full name of the identifier we are interested in
+
+ :type context_file: str or None
+ :param context_file:
+ context file to consider, necessary if the identifier has been
+ introduced using a relative import unresolvable in the actual
+ context (i.e. modutils)
+
+
+ :raise ImportError: if there is no such module in the directory
+
+ :rtype: str or None
+ :return:
+ the module part of the name or None if we have not been able at
+ all to import the given name
+
+ XXX: deprecated, since it doesn't handle package precedence over module
+ (see #10066)
+ """
+ # os.path trick
+ if dotted_name.startswith('os.path'):
+ return 'os.path'
+ parts = dotted_name.split('.')
+ if context_file is not None:
+ # first check for builtin module which won't be considered latter
+ # in that case (path != None)
+ if parts[0] in BUILTIN_MODULES:
+ if len(parts) > 2:
+ raise ImportError(dotted_name)
+ return parts[0]
+ # don't use += or insert, we want a new list to be created !
+ path = None
+ starti = 0
+ if parts[0] == '':
+ assert context_file is not None, \
+ 'explicit relative import, but no context_file?'
+ path = [] # prevent resolving the import non-relatively
+ starti = 1
+ while parts[starti] == '': # for all further dots: change context
+ starti += 1
+ context_file = dirname(context_file)
+ for i in range(starti, len(parts)):
+ try:
+ file_from_modpath(parts[starti:i+1], path=path,
+ context_file=context_file)
+ except ImportError:
+ if not i >= max(1, len(parts) - 2):
+ raise
+ return '.'.join(parts[:i])
+ return dotted_name
+
+
+def get_module_files(src_directory, blacklist):
+ """given a package directory return a list of all available python
+ module's files in the package and its subpackages
+
+ :type src_directory: str
+ :param src_directory:
+ path of the directory corresponding to the package
+
+ :type blacklist: list or tuple
+ :param blacklist:
+ optional list of files or directory to ignore, default to the value of
+ `logilab.common.STD_BLACKLIST`
+
+ :rtype: list
+ :return:
+ the list of all available python module's files in the package and
+ its subpackages
+ """
+ files = []
+ for directory, dirnames, filenames in os.walk(src_directory):
+ _handle_blacklist(blacklist, dirnames, filenames)
+ # check for __init__.py
+ if not '__init__.py' in filenames:
+ dirnames[:] = ()
+ continue
+ for filename in filenames:
+ if _is_python_file(filename):
+ src = join(directory, filename)
+ files.append(src)
+ return files
+
+
+def get_source_file(filename, include_no_ext=False):
+ """given a python module's file name return the matching source file
+ name (the filename will be returned identically if it's a already an
+ absolute path to a python source file...)
+
+ :type filename: str
+ :param filename: python module's file name
+
+
+ :raise NoSourceFile: if no source file exists on the file system
+
+ :rtype: str
+ :return: the absolute path of the source file if it exists
+ """
+ base, orig_ext = splitext(abspath(filename))
+ for ext in PY_SOURCE_EXTS:
+ source_path = '%s.%s' % (base, ext)
+ if exists(source_path):
+ return source_path
+ if include_no_ext and not orig_ext and exists(base):
+ return base
+ raise NoSourceFile(filename)
+
+
+def is_python_source(filename):
+ """
+ rtype: bool
+ return: True if the filename is a python source file
+ """
+ return splitext(filename)[1][1:] in PY_SOURCE_EXTS
+
+
+def is_standard_module(modname, std_path=(STD_LIB_DIR,)):
+ """try to guess if a module is a standard python module (by default,
+ see `std_path` parameter's description)
+
+ :type modname: str
+ :param modname: name of the module we are interested in
+
+ :type std_path: list(str) or tuple(str)
+ :param std_path: list of path considered has standard
+
+
+ :rtype: bool
+ :return:
+ true if the module:
+ - is located on the path listed in one of the directory in `std_path`
+ - is a built-in module
+ """
+ modname = modname.split('.')[0]
+ try:
+ filename = file_from_modpath([modname])
+ except ImportError:
+ # import failed, i'm probably not so wrong by supposing it's
+ # not standard...
+ return False
+ # modules which are not living in a file are considered standard
+ # (sys and __builtin__ for instance)
+ if filename is None:
+ return True
+ filename = abspath(filename)
+ if filename.startswith(EXT_LIB_DIR):
+ return False
+ for path in std_path:
+ if filename.startswith(_abspath(path)):
+ return True
+ return False
+
+
+
+def is_relative(modname, from_file):
+ """return true if the given module name is relative to the given
+ file name
+
+ :type modname: str
+ :param modname: name of the module we are interested in
+
+ :type from_file: str
+ :param from_file:
+ path of the module from which modname has been imported
+
+ :rtype: bool
+ :return:
+ true if the module has been imported relatively to `from_file`
+ """
+ if not isdir(from_file):
+ from_file = dirname(from_file)
+ if from_file in sys.path:
+ return False
+ try:
+ find_module(modname.split('.')[0], [from_file])
+ return True
+ except ImportError:
+ return False
+
+
+# internal only functions #####################################################
+
+def _file_from_modpath(modpath, path=None, context=None):
+ """given a mod path (i.e. splitted module / package name), return the
+ corresponding file
+
+ this function is used internally, see `file_from_modpath`'s
+ documentation for more information
+ """
+ assert len(modpath) > 0
+ if context is not None:
+ try:
+ mtype, mp_filename = _module_file(modpath, [context])
+ except ImportError:
+ mtype, mp_filename = _module_file(modpath, path)
+ else:
+ mtype, mp_filename = _module_file(modpath, path)
+ if mtype == PY_COMPILED:
+ try:
+ return get_source_file(mp_filename)
+ except NoSourceFile:
+ return mp_filename
+ elif mtype == C_BUILTIN:
+ # integrated builtin module
+ return None
+ elif mtype == PKG_DIRECTORY:
+ mp_filename = _has_init(mp_filename)
+ return mp_filename
+
+def _search_zip(modpath, pic):
+ for filepath, importer in pic.items():
+ if importer is not None:
+ if importer.find_module(modpath[0]):
+ if not importer.find_module(os.path.sep.join(modpath)):
+ raise ImportError('No module named %s in %s/%s' % (
+ '.'.join(modpath[1:]), filepath, modpath))
+ return ZIPFILE, abspath(filepath) + os.path.sep + os.path.sep.join(modpath), filepath
+ raise ImportError('No module named %s' % '.'.join(modpath))
+
+
+def _abspath(path, _abspathcache={}): #pylint: disable=dangerous-default-value
+ """abspath with caching"""
+ # _module_file calls abspath on every path in sys.path every time it's
+ # called; on a larger codebase this easily adds up to half a second just
+ # assembling path components. This cache alleviates that.
+ try:
+ return _abspathcache[path]
+ except KeyError:
+ if not path: # don't cache result for ''
+ return abspath(path)
+ _abspathcache[path] = abspath(path)
+ return _abspathcache[path]
+
+try:
+ import pkg_resources
+except ImportError:
+ pkg_resources = None
+
+def _module_file(modpath, path=None):
+ """get a module type / file path
+
+ :type modpath: list or tuple
+ :param modpath:
+ splitted module's name (i.e name of a module or package splitted
+ on '.'), with leading empty strings for explicit relative import
+
+ :type path: list or None
+ :param path:
+ optional list of path where the module or package should be
+ searched (use sys.path if nothing or None is given)
+
+
+ :rtype: tuple(int, str)
+ :return: the module type flag and the file path for a module
+ """
+ # egg support compat
+ try:
+ pic = sys.path_importer_cache
+ _path = (path is None and sys.path or path)
+ for __path in _path:
+ if not __path in pic:
+ try:
+ pic[__path] = zipimport.zipimporter(__path)
+ except zipimport.ZipImportError:
+ pic[__path] = None
+ checkeggs = True
+ except AttributeError:
+ checkeggs = False
+ # pkg_resources support (aka setuptools namespace packages)
+ if pkg_resources is not None and modpath[0] in pkg_resources._namespace_packages and len(modpath) > 1:
+ # setuptools has added into sys.modules a module object with proper
+ # __path__, get back information from there
+ module = sys.modules[modpath.pop(0)]
+ path = module.__path__
+ imported = []
+ while modpath:
+ modname = modpath[0]
+ # take care to changes in find_module implementation wrt builtin modules
+ #
+ # Python 2.6.6 (r266:84292, Sep 11 2012, 08:34:23)
+ # >>> imp.find_module('posix')
+ # (None, 'posix', ('', '', 6))
+ #
+ # Python 3.3.1 (default, Apr 26 2013, 12:08:46)
+ # >>> imp.find_module('posix')
+ # (None, None, ('', '', 6))
+ try:
+ _, mp_filename, mp_desc = find_module(modname, path)
+ except ImportError:
+ if checkeggs:
+ return _search_zip(modpath, pic)[:2]
+ raise
+ else:
+ if checkeggs and mp_filename:
+ fullabspath = [_abspath(x) for x in _path]
+ try:
+ pathindex = fullabspath.index(dirname(abspath(mp_filename)))
+ emtype, emp_filename, zippath = _search_zip(modpath, pic)
+ if pathindex > _path.index(zippath):
+ # an egg takes priority
+ return emtype, emp_filename
+ except ValueError:
+ # XXX not in _path
+ pass
+ except ImportError:
+ pass
+ checkeggs = False
+ imported.append(modpath.pop(0))
+ mtype = mp_desc[2]
+ if modpath:
+ if mtype != PKG_DIRECTORY:
+ raise ImportError('No module %s in %s' % ('.'.join(modpath),
+ '.'.join(imported)))
+ # XXX guess if package is using pkgutil.extend_path by looking for
+ # those keywords in the first four Kbytes
+ try:
+ with open(join(mp_filename, '__init__.py')) as stream:
+ data = stream.read(4096)
+ except IOError:
+ path = [mp_filename]
+ else:
+ if 'pkgutil' in data and 'extend_path' in data:
+ # extend_path is called, search sys.path for module/packages
+ # of this name see pkgutil.extend_path documentation
+ path = [join(p, *imported) for p in sys.path
+ if isdir(join(p, *imported))]
+ else:
+ path = [mp_filename]
+ return mtype, mp_filename
+
+def _is_python_file(filename):
+ """return true if the given filename should be considered as a python file
+
+ .pyc and .pyo are ignored
+ """
+ for ext in ('.py', '.so', '.pyd', '.pyw'):
+ if filename.endswith(ext):
+ return True
+ return False
+
+
+def _has_init(directory):
+ """if the given directory has a valid __init__ file, return its path,
+ else return None
+ """
+ mod_or_pack = join(directory, '__init__')
+ for ext in PY_SOURCE_EXTS + ('pyc', 'pyo'):
+ if exists(mod_or_pack + '.' + ext):
+ return mod_or_pack + '.' + ext
+ return None
diff --git a/node_classes.py b/node_classes.py
index 45a00ba9..3c765bb4 100644
--- a/node_classes.py
+++ b/node_classes.py
@@ -24,9 +24,11 @@ from logilab.common.decorators import cachedproperty
from astroid.exceptions import NoDefault
from astroid.bases import (NodeNG, Statement, Instance, InferenceContext,
- _infer_stmts, YES, BUILTINS)
-from astroid.mixins import BlockRangeMixIn, AssignTypeMixin, \
- ParentAssignTypeMixin, FromImportMixIn
+ _infer_stmts, YES, BUILTINS)
+from astroid.mixins import (BlockRangeMixIn, AssignTypeMixin,
+ ParentAssignTypeMixin, FromImportMixIn)
+
+PY3K = sys.version_info >= (3, 0)
def unpack_infer(stmt, context=None):
@@ -84,16 +86,16 @@ def are_exclusive(stmt1, stmt2, exceptions=None):
# nodes are in exclusive branches
if isinstance(node, If) and exceptions is None:
if (node.locate_child(previous)[1]
- is not node.locate_child(children[node])[1]):
+ is not node.locate_child(children[node])[1]):
return True
elif isinstance(node, TryExcept):
c2attr, c2node = node.locate_child(previous)
c1attr, c1node = node.locate_child(children[node])
if c1node is not c2node:
if ((c2attr == 'body' and c1attr == 'handlers' and children[node].catch(exceptions)) or
- (c2attr == 'handlers' and c1attr == 'body' and previous.catch(exceptions)) or
- (c2attr == 'handlers' and c1attr == 'orelse') or
- (c2attr == 'orelse' and c1attr == 'handlers')):
+ (c2attr == 'handlers' and c1attr == 'body' and previous.catch(exceptions)) or
+ (c2attr == 'handlers' and c1attr == 'orelse') or
+ (c2attr == 'orelse' and c1attr == 'handlers')):
return True
elif c2attr == 'handlers' and c1attr == 'handlers':
return previous is not children[node]
@@ -110,13 +112,13 @@ class LookupMixIn(object):
def lookup(self, name):
"""lookup a variable name
- return the scope node and the list of assignments associated to the given
- name according to the scope where it has been found (locals, globals or
- builtin)
+ return the scope node and the list of assignments associated to the
+ given name according to the scope where it has been found (locals,
+ globals or builtin)
- The lookup is starting from self's scope. If self is not a frame itself and
- the name is found in the inner frame locals, statements will be filtered
- to remove ignorable statements according to self's location
+ The lookup is starting from self's scope. If self is not a frame itself
+ and the name is found in the inner frame locals, statements will be
+ filtered to remove ignorable statements according to self's location
"""
return self.scope().scope_lookup(self, name)
@@ -254,7 +256,26 @@ class Name(LookupMixIn, NodeNG):
class Arguments(NodeNG, AssignTypeMixin):
"""class representing an Arguments node"""
- _astroid_fields = ('args', 'defaults', 'kwonlyargs', 'kw_defaults')
+ if PY3K:
+ # Python 3.4+ uses a different approach regarding annotations,
+ # each argument is a new class, _ast.arg, which exposes an
+ # 'annotation' attribute. In astroid though, arguments are exposed
+ # as is in the Arguments node and the only way to expose annotations
+ # is by using something similar with Python 3.3:
+ # - we expose 'varargannotation' and 'kwargannotation' of annotations
+ # of varargs and kwargs.
+ # - we expose 'annotation', a list with annotations for
+ # for each normal argument. If an argument doesn't have an
+ # annotation, its value will be None.
+
+ _astroid_fields = ('args', 'defaults', 'kwonlyargs',
+ 'kw_defaults', 'annotations',
+ 'varargannotation', 'kwargannotation')
+ annotations = None
+ varargannotation = None
+ kwargannotation = None
+ else:
+ _astroid_fields = ('args', 'defaults', 'kwonlyargs', 'kw_defaults')
args = None
defaults = None
kwonlyargs = None
@@ -429,7 +450,7 @@ class Compare(NodeNG):
class Comprehension(NodeNG):
"""class representing a Comprehension node"""
- _astroid_fields = ('target', 'iter' ,'ifs')
+ _astroid_fields = ('target', 'iter', 'ifs')
target = None
iter = None
ifs = None
@@ -513,7 +534,7 @@ class Dict(NodeNG, Instance):
self.items = []
else:
self.items = [(const_factory(k), const_factory(v))
- for k,v in items.iteritems()]
+ for k, v in items.iteritems()]
def pytype(self):
return '%s.dict' % BUILTINS
@@ -539,7 +560,8 @@ class Dict(NodeNG, Instance):
for inferedkey in key.infer(context):
if inferedkey is YES:
continue
- if isinstance(inferedkey, Const) and inferedkey.value == lookup_key:
+ if isinstance(inferedkey, Const) \
+ and inferedkey.value == lookup_key:
return value
# This should raise KeyError, but all call sites only catch
# IndexError. Let's leave it like that for now.
@@ -614,7 +636,7 @@ class For(BlockRangeMixIn, AssignTypeMixin, Statement):
class From(FromImportMixIn, Statement):
"""class representing a From node"""
- def __init__(self, fromname, names, level=0):
+ def __init__(self, fromname, names, level=0):
self.modname = fromname
self.names = names
self.level = level
@@ -818,7 +840,7 @@ class TryFinally(BlockRangeMixIn, Statement):
child = self.body[0]
# py2.5 try: except: finally:
if (isinstance(child, TryExcept) and child.fromlineno == self.fromlineno
- and lineno > self.fromlineno and lineno <= child.tolineno):
+ and lineno > self.fromlineno and lineno <= child.tolineno):
return child.block_range(lineno)
return self._elsed_block_range(lineno, self.finalbody)
diff --git a/protocols.py b/protocols.py
index f486e7cc..e1816a71 100644
--- a/protocols.py
+++ b/protocols.py
@@ -70,7 +70,7 @@ BIN_OP_IMPL = {'+': lambda a, b: a + b,
'^': lambda a, b: a ^ b,
'<<': lambda a, b: a << b,
'>>': lambda a, b: a >> b,
- }
+ }
for key, impl in BIN_OP_IMPL.items():
BIN_OP_IMPL[key+'='] = impl
@@ -166,7 +166,7 @@ def _resolve_looppart(parts, asspath, context):
assigned = stmt.getitem(index, context)
except (AttributeError, IndexError):
continue
- except TypeError, exc: # stmt is unsubscriptable Const
+ except TypeError: # stmt is unsubscriptable Const
continue
if not asspath:
# we achieved to resolved the assignment path,
@@ -231,10 +231,14 @@ def _arguments_infer_argname(self, name, context):
yield self.parent.parent.frame()
return
if name == self.vararg:
- yield const_factory(())
+ vararg = const_factory(())
+ vararg.parent = self
+ yield vararg
return
if name == self.kwarg:
- yield const_factory({})
+ kwarg = const_factory({})
+ kwarg.parent = self
+ yield kwarg
return
# if there is a default value, yield it. And then yield YES to reflect
# we can't guess given argument value
diff --git a/raw_building.py b/raw_building.py
index bb685a9e..e245f912 100644
--- a/raw_building.py
+++ b/raw_building.py
@@ -28,7 +28,7 @@ from inspect import (getargspec, isdatadescriptor, isfunction, ismethod,
from astroid.node_classes import CONST_CLS
from astroid.nodes import (Module, Class, Const, const_factory, From,
- Function, EmptyNode, Name, Arguments)
+ Function, EmptyNode, Name, Arguments)
from astroid.bases import BUILTINS, Generator
from astroid.manager import AstroidManager
MANAGER = AstroidManager()
@@ -258,9 +258,9 @@ class InspectBuilder(object):
attach_dummy_node(node, name, member)
else:
object_build_function(node, member, name)
- elif isbuiltin(member):
+ elif isbuiltin(member):
if (not _io_discrepancy(member) and
- self.imported_member(node, member, name)):
+ self.imported_member(node, member, name)):
#if obj is object:
# print 'skippp', obj, name, member
continue
@@ -325,11 +325,11 @@ class InspectBuilder(object):
return False
-### astroid boot strapping ################################################### ###
+### astroid bootstrapping ######################################################
Astroid_BUILDER = InspectBuilder()
_CONST_PROXY = {}
-def astroid_boot_strapping():
+def astroid_bootstrapping():
"""astroid boot strapping the builtins module"""
# this boot strapping is necessary since we need the Const nodes to
# inspect_build builtins, and then we can proxy Const
@@ -346,7 +346,7 @@ def astroid_boot_strapping():
else:
_CONST_PROXY[cls] = proxy
-astroid_boot_strapping()
+astroid_bootstrapping()
# TODO : find a nicer way to handle this situation;
# However __proxied introduced an
diff --git a/rebuilder.py b/rebuilder.py
index c581eb98..95790314 100644
--- a/rebuilder.py
+++ b/rebuilder.py
@@ -20,8 +20,8 @@ order to get a single Astroid representation
"""
import sys
-from warnings import warn
-from _ast import (Expr as Discard, Str, Name, Attribute,
+from _ast import (
+ Expr as Discard, Str,
# binary operators
Add, Div, FloorDiv, Mod, Mult, Pow, Sub, BitAnd, BitOr, BitXor,
LShift, RShift,
@@ -47,15 +47,18 @@ _BIN_OP_CLASSES = {Add: '+',
Pow: '**',
Sub: '-',
LShift: '<<',
- RShift: '>>'}
+ RShift: '>>',
+ }
_BOOL_OP_CLASSES = {And: 'and',
- Or: 'or'}
+ Or: 'or',
+ }
_UNARY_OP_CLASSES = {UAdd: '+',
USub: '-',
Not: 'not',
- Invert: '~'}
+ Invert: '~',
+ }
_CMP_OP_CLASSES = {Eq: '==',
Gt: '>',
@@ -66,11 +69,13 @@ _CMP_OP_CLASSES = {Eq: '==',
Lt: '<',
LtE: '<=',
NotEq: '!=',
- NotIn: 'not in'}
+ NotIn: 'not in',
+ }
CONST_NAME_TRANSFORMS = {'None': None,
'True': True,
- 'False': False}
+ 'False': False,
+ }
REDIRECT = {'arguments': 'Arguments',
'Attribute': 'Getattr',
@@ -86,7 +91,7 @@ REDIRECT = {'arguments': 'Arguments',
'ImportFrom': 'From',
'keyword': 'Keyword',
'Repr': 'Backquote',
- }
+ }
PY3K = sys.version_info >= (3, 0)
PY34 = sys.version_info >= (3, 4)
@@ -174,10 +179,25 @@ class TreeRebuilder(object):
vararg, kwarg = node.vararg, node.kwarg
# change added in 82732 (7c5c678e4164), vararg and kwarg
# are instances of `_ast.arg`, not strings
- if vararg and PY34:
- vararg = vararg.arg
- if kwarg and PY34:
- kwarg = kwarg.arg
+ if vararg:
+ if PY34:
+ if vararg.annotation:
+ newnode.varargannotation = self.visit(vararg.annotation,
+ newnode)
+ vararg = vararg.arg
+ elif PY3K and node.varargannotation:
+ newnode.varargannotation = self.visit(node.varargannotation,
+ newnode)
+ if kwarg:
+ if PY34:
+ if kwarg.annotation:
+ newnode.kwargannotation = self.visit(kwarg.annotation,
+ newnode)
+ kwarg = kwarg.arg
+ elif PY3K:
+ if node.kwargannotation:
+ newnode.kwargannotation = self.visit(node.kwargannotation,
+ newnode)
newnode.vararg = vararg
newnode.kwarg = kwarg
# save argument names in locals:
@@ -217,8 +237,8 @@ class TreeRebuilder(object):
# set some function or metaclass infos XXX explain ?
klass = newnode.parent.frame()
if (isinstance(klass, new.Class)
- and isinstance(newnode.value, new.CallFunc)
- and isinstance(newnode.value.func, new.Name)):
+ and isinstance(newnode.value, new.CallFunc)
+ and isinstance(newnode.value.func, new.Name)):
func_name = newnode.value.func.name
for ass_node in newnode.targets:
try:
@@ -326,7 +346,7 @@ class TreeRebuilder(object):
_lineno_parent(node, newnode, parent)
newnode.left = self.visit(node.left, newnode)
newnode.ops = [(_CMP_OP_CLASSES[op.__class__], self.visit(expr, newnode))
- for (op, expr) in zip(node.ops, node.comparators)]
+ for (op, expr) in zip(node.ops, node.comparators)]
return newnode
def visit_comprehension(self, node, parent):
@@ -349,7 +369,7 @@ class TreeRebuilder(object):
if 'decorators' in node._fields: # py < 2.6, i.e. 2.5
decorators = node.decorators
else:
- decorators= node.decorator_list
+ decorators = node.decorator_list
newnode.nodes = [self.visit(child, newnode) for child in decorators]
return newnode
@@ -367,7 +387,7 @@ class TreeRebuilder(object):
newnode = new.Dict()
_lineno_parent(node, newnode, parent)
newnode.items = [(self.visit(key, newnode), self.visit(value, newnode))
- for key, value in zip(node.keys, node.values)]
+ for key, value in zip(node.keys, node.values)]
return newnode
def visit_dictcomp(self, node, parent):
@@ -467,6 +487,8 @@ class TreeRebuilder(object):
decorators = getattr(node, attr)
if decorators:
newnode.decorators = self.visit_decorators(node, newnode)
+ if PY3K and node.returns:
+ newnode.returns = self.visit(node.returns, newnode)
self._global_names.pop()
frame = newnode.parent.frame()
if isinstance(frame, new.Class):
@@ -782,6 +804,9 @@ class TreeRebuilder3k(TreeRebuilder):
newnode.kwonlyargs = [self.visit(child, newnode) for child in node.kwonlyargs]
self.asscontext = None
newnode.kw_defaults = [self.visit(child, newnode) if child else None for child in node.kw_defaults]
+ newnode.annotations = [
+ self.visit(arg.annotation, newnode) if arg.annotation else None
+ for arg in node.args]
return newnode
def visit_excepthandler(self, node, parent):
diff --git a/scoped_nodes.py b/scoped_nodes.py
index 7e6349df..61d00bd7 100644
--- a/scoped_nodes.py
+++ b/scoped_nodes.py
@@ -37,7 +37,7 @@ from astroid.exceptions import NotFoundError, \
AstroidBuildingException, InferenceError
from astroid.node_classes import Const, DelName, DelAttr, \
Dict, From, List, Pass, Raise, Return, Tuple, Yield, YieldFrom, \
- LookupMixIn, const_factory as cf, unpack_infer, Name
+ LookupMixIn, const_factory as cf, unpack_infer, Name, CallFunc
from astroid.bases import NodeNG, InferenceContext, Instance,\
YES, Generator, UnboundMethod, BoundMethod, _infer_stmts, \
BUILTINS
@@ -45,6 +45,9 @@ from astroid.mixins import FilterStmtsMixin
from astroid.bases import Statement
from astroid.manager import AstroidManager
+ITER_METHODS = ('__iter__', '__getitem__')
+PY3K = sys.version_info >= (3, 0)
+
def remove_nodes(func, cls):
def wrapper(*args, **kwargs):
@@ -481,6 +484,48 @@ else:
# Function ###################################################################
+def _infer_decorator_callchain(node):
+ """ Detect decorator call chaining and see if the
+ end result is a static or a classmethod.
+ """
+ current = node
+ while True:
+ if isinstance(current, CallFunc):
+ try:
+ current = current.func.infer().next()
+ except InferenceError:
+ return
+ elif isinstance(current, Function):
+ if not current.parent:
+ return
+ try:
+ # TODO: We don't handle multiple inference results right now,
+ # because there's no flow to reason when the return
+ # is what we are looking for, a static or a class method.
+ result = current.infer_call_result(current.parent).next()
+ except (StopIteration, InferenceError):
+ return
+ if isinstance(result, (Function, CallFunc)):
+ current = result
+ else:
+ if isinstance(result, Instance):
+ result = result._proxied
+ if isinstance(result, Class):
+ if (result.name == 'classmethod' and
+ result.root().name == BUILTINS):
+ return 'classmethod'
+ elif (result.name == 'staticmethod' and
+ result.root().name == BUILTINS):
+ return 'staticmethod'
+ else:
+ return
+ else:
+ # We aren't interested in anything else returned,
+ # so go back to the function type inference.
+ return
+ else:
+ return
+
def _function_type(self):
"""
Function type, possible values are:
@@ -491,6 +536,12 @@ def _function_type(self):
# so do it here.
if self.decorators:
for node in self.decorators.nodes:
+ if isinstance(node, CallFunc):
+ _type = _infer_decorator_callchain(node)
+ if _type is None:
+ continue
+ else:
+ return _type
if not isinstance(node, Name):
continue
try:
@@ -500,7 +551,7 @@ def _function_type(self):
for ancestor in infered.ancestors():
if isinstance(ancestor, Class):
if (ancestor.name == 'classmethod' and
- ancestor.root().name == BUILTINS):
+ ancestor.root().name == BUILTINS):
return 'classmethod'
elif (ancestor.name == 'staticmethod' and
ancestor.root().name == BUILTINS):
@@ -564,7 +615,11 @@ class Lambda(LocalsDictNodeNG, FilterStmtsMixin):
class Function(Statement, Lambda):
- _astroid_fields = ('decorators', 'args', 'body')
+ if PY3K:
+ _astroid_fields = ('decorators', 'args', 'body', 'returns')
+ returns = None
+ else:
+ _astroid_fields = ('decorators', 'args', 'body')
special_attributes = set(('__name__', '__doc__', '__dict__'))
is_function = True
@@ -760,12 +815,12 @@ def _class_type(klass, ancestors=None):
for base in klass.ancestors(recurs=False):
name = _class_type(base, ancestors)
if name != 'class':
- if name == 'metaclass' and not _is_metaclass(klass):
- # don't propagate it if the current class
- # can't be a metaclass
- continue
- klass._type = base.type
- break
+ if name == 'metaclass' and not _is_metaclass(klass):
+ # don't propagate it if the current class
+ # can't be a metaclass
+ continue
+ klass._type = base.type
+ break
if klass._type is None:
klass._type = 'class'
return klass._type
@@ -851,9 +906,36 @@ class Class(Statement, LocalsDictNodeNG, FilterStmtsMixin):
def callable(self):
return True
+ def _is_subtype_of(self, type_name):
+ if self.qname() == type_name:
+ return True
+ for anc in self.ancestors():
+ if anc.qname() == type_name:
+ return True
+
def infer_call_result(self, caller, context=None):
"""infer what a class is returning when called"""
- yield Instance(self)
+ if self._is_subtype_of('%s.type' % (BUILTINS,)) and len(caller.args) == 3:
+ name_node = caller.args[0].infer().next()
+ if isinstance(name_node, Const) and isinstance(name_node.value, basestring):
+ name = name_node.value
+ else:
+ yield YES
+ return
+ result = Class(name, None)
+ bases = caller.args[1].infer().next()
+ if isinstance(bases, (Tuple, List)):
+ result.bases = bases.itered()
+ else:
+ # There is currently no AST node that can represent an 'unknown'
+ # node (YES is not an AST node), therefore we simply return YES here
+ # although we know at least the name of the class.
+ yield YES
+ return
+ result.parent = caller.parent
+ yield result
+ else:
+ yield Instance(self)
def scope_lookup(self, node, name, offset=0):
if node in self.bases:
@@ -1110,7 +1192,7 @@ class Class(Statement, LocalsDictNodeNG, FilterStmtsMixin):
return None
elif '__metaclass__' in self.root().locals:
assignments = [ass for ass in self.root().locals['__metaclass__']
- if ass.lineno < self.lineno]
+ if ass.lineno < self.lineno]
if not assignments:
return None
assignment = assignments[-1]
@@ -1139,3 +1221,55 @@ class Class(Statement, LocalsDictNodeNG, FilterStmtsMixin):
if klass is not None:
break
return klass
+
+ def _islots(self):
+ """ Return an iterator with the inferred slots. """
+ if '__slots__' not in self.locals:
+ return
+ for slots in self.igetattr('__slots__'):
+ # check if __slots__ is a valid type
+ for meth in ITER_METHODS:
+ try:
+ slots.getattr(meth)
+ break
+ except NotFoundError:
+ continue
+ else:
+ continue
+
+ if isinstance(slots, Const):
+ # a string. Ignore the following checks,
+ # but yield the node, only if it has a value
+ if slots.value:
+ yield slots
+ continue
+ if not hasattr(slots, 'itered'):
+ # we can't obtain the values, maybe a .deque?
+ continue
+
+ if isinstance(slots, Dict):
+ values = [item[0] for item in slots.items]
+ else:
+ values = slots.itered()
+ if values is YES:
+ continue
+
+ for elt in values:
+ try:
+ for infered in elt.infer():
+ if infered is YES:
+ continue
+ if (not isinstance(infered, Const) or
+ not isinstance(infered.value, str)):
+ continue
+ if not infered.value:
+ continue
+ yield infered
+ except InferenceError:
+ continue
+
+ # Cached, because inferring them all the time is expensive
+ @cached
+ def slots(self):
+ """ Return all the slots for this node. """
+ return list(self._islots())
diff --git a/test/data/find_test/__init__.py b/test/data/find_test/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/test/data/find_test/__init__.py
diff --git a/test/data/find_test/module.py b/test/data/find_test/module.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/test/data/find_test/module.py
diff --git a/test/data/find_test/module2.py b/test/data/find_test/module2.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/test/data/find_test/module2.py
diff --git a/test/data/find_test/noendingnewline.py b/test/data/find_test/noendingnewline.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/test/data/find_test/noendingnewline.py
diff --git a/test/data/find_test/nonregr.py b/test/data/find_test/nonregr.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/test/data/find_test/nonregr.py
diff --git a/test/data/lmfp/__init__.py b/test/data/lmfp/__init__.py
new file mode 100644
index 00000000..74b26b82
--- /dev/null
+++ b/test/data/lmfp/__init__.py
@@ -0,0 +1,2 @@
+# force a "direct" python import
+from . import foo
diff --git a/test/data/lmfp/foo.py b/test/data/lmfp/foo.py
new file mode 100644
index 00000000..8f7de1e8
--- /dev/null
+++ b/test/data/lmfp/foo.py
@@ -0,0 +1,6 @@
+import sys
+if not getattr(sys, 'bar', None):
+ sys.just_once = []
+# there used to be two numbers here because
+# of a load_module_from_path bug
+sys.just_once.append(42)
diff --git a/test/data/module.py b/test/data/module.py
index 8f7da02d..fb48dc1d 100644
--- a/test/data/module.py
+++ b/test/data/module.py
@@ -2,8 +2,8 @@
"""
__revision__ = '$Id: module.py,v 1.2 2005-11-02 11:56:54 syt Exp $'
-from logilab.common import modutils
from logilab.common.shellutils import ProgressBar as pb
+from astroid import modutils
from astroid.utils import *
import os.path
MY_DICT = {}
diff --git a/test/data/module1abs/__init__.py b/test/data/module1abs/__init__.py
new file mode 100644
index 00000000..9eb5902a
--- /dev/null
+++ b/test/data/module1abs/__init__.py
@@ -0,0 +1,4 @@
+from __future__ import absolute_import
+from . import core
+from .core import *
+print sys.version
diff --git a/test/data/module1abs/core.py b/test/data/module1abs/core.py
new file mode 100644
index 00000000..de101117
--- /dev/null
+++ b/test/data/module1abs/core.py
@@ -0,0 +1 @@
+import sys
diff --git a/test/unittest_inference.py b/test/unittest_inference.py
index db950e38..285b8764 100644
--- a/test/unittest_inference.py
+++ b/test/unittest_inference.py
@@ -497,7 +497,7 @@ class Warning(Warning):
def test_qqch(self):
code = '''
-from logilab.common.modutils import load_module_from_name
+from astroid.modutils import load_module_from_name
xxx = load_module_from_name('__pkginfo__')
'''
astroid = builder.string_build(code, __name__, __file__)
@@ -1220,5 +1220,25 @@ empty_list = A().empty_method()
empty_list = astroid['empty_list'].infered()[0]
self.assertIsInstance(empty_list, nodes.List)
+ def test_infer_variable_arguments(self):
+ code = '''
+def test(*args, **kwargs):
+ vararg = args
+ kwarg = kwargs
+ '''
+ astroid = builder.string_build(code, __name__, __file__)
+ func = astroid['test']
+ vararg = func.body[0].value
+ kwarg = func.body[1].value
+
+ kwarg_infered = kwarg.infered()[0]
+ self.assertIsInstance(kwarg_infered, nodes.Dict)
+ self.assertIs(kwarg_infered.parent, func.args)
+
+ vararg_infered = vararg.infered()[0]
+ self.assertIsInstance(vararg_infered, nodes.Tuple)
+ self.assertIs(vararg_infered.parent, func.args)
+
+
if __name__ == '__main__':
unittest_main()
diff --git a/test/unittest_manager.py b/test/unittest_manager.py
index 5db186dd..6b3c4895 100644
--- a/test/unittest_manager.py
+++ b/test/unittest_manager.py
@@ -1,4 +1,4 @@
-# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of astroid.
@@ -22,6 +22,7 @@ from os.path import join, abspath, dirname
from astroid.manager import AstroidManager, _silent_no_wrap
from astroid.bases import BUILTINS
from astroid.exceptions import AstroidBuildingException
+from astroid.raw_building import astroid_bootstrapping
DATA = join(dirname(abspath(__file__)), 'data')
PY3K = sys.version_info > (3, 0)
@@ -29,7 +30,7 @@ PY3K = sys.version_info > (3, 0)
class AstroidManagerTC(TestCase):
def setUp(self):
self.manager = AstroidManager()
- self.manager.astroid_cache.clear()
+ self.manager.clear_cache() # take care of borg
def test_ast_from_file(self):
"""check if the method return a good astroid object"""
@@ -90,9 +91,9 @@ class AstroidManagerTC(TestCase):
try:
module = self.manager.ast_from_module_name('mypypa')
self.assertEqual(module.name, 'mypypa')
- end = join(archive, 'mypypa')
+ end = join(archive, 'mypypa')
self.assertTrue(module.file.endswith(end),
- module.file)
+ "%s doesn't endswith %s" % (module.file, end))
finally:
# remove the module, else after importing egg, we don't get the zip
if 'mypypa' in self.manager.astroid_cache:
@@ -119,7 +120,7 @@ class AstroidManagerTC(TestCase):
self.assertEqual(self.manager.zip_import_data('path'), None)
def test_file_from_module(self):
- """check if the unittest filepath is equals to the result of the method"""
+ """check if the unittest filepath is equals to the result of the method"""
import unittest
if PY3K:
unittest_file = unittest.__file__
@@ -181,11 +182,22 @@ class AstroidManagerTC(TestCase):
obj = self.manager.project_from_files([DATA], _silent_no_wrap, 'data')
expected = set(['SSL1', '__init__', 'all', 'appl', 'format', 'module',
'module2', 'noendingnewline', 'nonregr', 'notall'])
- expected = ['data', 'data.SSL1', 'data.SSL1.Connection1',
- 'data.absimport', 'data.all',
- 'data.appl', 'data.appl.myConnection', 'data.email', 'data.format',
- 'data.module', 'data.module2', 'data.noendingnewline',
- 'data.nonregr', 'data.notall']
+ expected = [
+ 'data', 'data.SSL1', 'data.SSL1.Connection1',
+ 'data.absimport', 'data.all',
+ 'data.appl', 'data.appl.myConnection', 'data.email',
+ 'data.find_test',
+ 'data.find_test.module',
+ 'data.find_test.module2',
+ 'data.find_test.noendingnewline',
+ 'data.find_test.nonregr',
+ 'data.format',
+ 'data.lmfp',
+ 'data.lmfp.foo',
+ 'data.module',
+ 'data.module1abs', 'data.module1abs.core',
+ 'data.module2', 'data.noendingnewline',
+ 'data.nonregr', 'data.notall']
self.assertListEqual(sorted(k for k in obj.keys()), expected)
class BorgAstroidManagerTC(TestCase):
diff --git a/test/unittest_modutils.py b/test/unittest_modutils.py
new file mode 100644
index 00000000..a9cd6245
--- /dev/null
+++ b/test/unittest_modutils.py
@@ -0,0 +1,282 @@
+# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of astroid.
+#
+# astroid is free software: you can redistribute it and/or modify it under
+# the terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option) any
+# later version.
+#
+# astroid 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 Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with astroid. If not, see <http://www.gnu.org/licenses/>.
+"""
+unit tests for module modutils (module manipulation utilities)
+"""
+
+import sys
+try:
+ __file__
+except NameError:
+ __file__ = sys.argv[0]
+
+from logilab.common.testlib import TestCase, unittest_main
+
+from os import path, getcwd, sep
+from astroid import modutils
+
+sys.path.insert(0, path.dirname(__file__))
+DATADIR = path.abspath(path.normpath(path.join(path.dirname(__file__), 'data')))
+
+
+class ModuleFileTC(TestCase):
+ package = "mypypa"
+
+ def tearDown(self):
+ super(ModuleFileTC, self).tearDown()
+ for k in sys.path_importer_cache.keys():
+ if 'MyPyPa' in k:
+ del sys.path_importer_cache[k]
+
+ def test_find_zipped_module(self):
+ mtype, mfile = modutils._module_file([self.package], [path.join(DATADIR, 'MyPyPa-0.1.0-py2.5.zip')])
+ self.assertEqual(mtype, modutils.ZIPFILE)
+ self.assertEqual(mfile.split(sep)[-4:], ["test", "data", "MyPyPa-0.1.0-py2.5.zip", self.package])
+
+ def test_find_egg_module(self):
+ mtype, mfile = modutils._module_file([self.package], [path.join(DATADIR, 'MyPyPa-0.1.0-py2.5.egg')])
+ self.assertEqual(mtype, modutils.ZIPFILE)
+ self.assertEqual(mfile.split(sep)[-4:], ["test", "data", "MyPyPa-0.1.0-py2.5.egg", self.package])
+
+
+class load_module_from_name_tc(TestCase):
+ """ load a python module from it's name """
+
+ def test_knownValues_load_module_from_name_1(self):
+ self.assertEqual(modutils.load_module_from_name('sys'), sys)
+
+ def test_knownValues_load_module_from_name_2(self):
+ self.assertEqual(modutils.load_module_from_name('os.path'), path)
+
+ def test_raise_load_module_from_name_1(self):
+ self.assertRaises(ImportError,
+ modutils.load_module_from_name, 'os.path', use_sys=0)
+
+
+class get_module_part_tc(TestCase):
+ """given a dotted name return the module part of the name"""
+
+ def test_knownValues_get_module_part_1(self):
+ self.assertEqual(modutils.get_module_part('astroid.modutils'),
+ 'astroid.modutils')
+
+ def test_knownValues_get_module_part_2(self):
+ self.assertEqual(modutils.get_module_part('astroid.modutils.get_module_part'),
+ 'astroid.modutils')
+
+ def test_knownValues_get_module_part_3(self):
+ """relative import from given file"""
+ self.assertEqual(modutils.get_module_part('node_classes.AssName',
+ modutils.__file__), 'node_classes')
+
+ def test_knownValues_get_compiled_module_part(self):
+ self.assertEqual(modutils.get_module_part('math.log10'), 'math')
+ self.assertEqual(modutils.get_module_part('math.log10', __file__), 'math')
+
+ def test_knownValues_get_builtin_module_part(self):
+ self.assertEqual(modutils.get_module_part('sys.path'), 'sys')
+ self.assertEqual(modutils.get_module_part('sys.path', '__file__'), 'sys')
+
+ def test_get_module_part_exception(self):
+ self.assertRaises(ImportError, modutils.get_module_part, 'unknown.module',
+ modutils.__file__)
+
+
+class modpath_from_file_tc(TestCase):
+ """ given an absolute file path return the python module's path as a list """
+
+ def test_knownValues_modpath_from_file_1(self):
+ self.assertEqual(modutils.modpath_from_file(modutils.__file__),
+ ['astroid', 'modutils'])
+
+ def test_knownValues_modpath_from_file_2(self):
+ self.assertEqual(modutils.modpath_from_file('unittest_modutils.py',
+ {getcwd(): 'arbitrary.pkg'}),
+ ['arbitrary', 'pkg', 'unittest_modutils'])
+
+ def test_raise_modpath_from_file_Exception(self):
+ self.assertRaises(Exception, modutils.modpath_from_file, '/turlututu')
+
+
+class load_module_from_path_tc(TestCase):
+
+ def test_do_not_load_twice(self):
+ sys.path.insert(0, self.datadir)
+ foo = modutils.load_module_from_modpath(['lmfp', 'foo'])
+ lmfp = modutils.load_module_from_modpath(['lmfp'])
+ self.assertEqual(len(sys.just_once), 1)
+ sys.path.pop(0)
+ del sys.just_once
+
+
+class file_from_modpath_tc(TestCase):
+ """given a mod path (i.e. splited module / package name), return the
+ corresponding file, giving priority to source file over precompiled file
+ if it exists"""
+
+ def test_site_packages(self):
+ self.assertEqual(path.realpath(modutils.file_from_modpath(['astroid', 'modutils'])),
+ path.realpath(modutils.__file__.replace('.pyc', '.py')))
+
+ def test_std_lib(self):
+ from os import path
+ self.assertEqual(path.realpath(modutils.file_from_modpath(['os', 'path']).replace('.pyc', '.py')),
+ path.realpath(path.__file__.replace('.pyc', '.py')))
+
+ def test_xmlplus(self):
+ try:
+ # don't fail if pyxml isn't installed
+ from xml.dom import ext
+ except ImportError:
+ pass
+ else:
+ self.assertEqual(path.realpath(modutils.file_from_modpath(['xml', 'dom', 'ext']).replace('.pyc', '.py')),
+ path.realpath(ext.__file__.replace('.pyc', '.py')))
+
+ def test_builtin(self):
+ self.assertEqual(modutils.file_from_modpath(['sys']),
+ None)
+
+
+ def test_unexisting(self):
+ self.assertRaises(ImportError, modutils.file_from_modpath, ['turlututu'])
+
+
+class get_source_file_tc(TestCase):
+
+ def test(self):
+ from os import path
+ self.assertEqual(modutils.get_source_file(path.__file__),
+ path.normpath(path.__file__.replace('.pyc', '.py')))
+
+ def test_raise(self):
+ self.assertRaises(modutils.NoSourceFile, modutils.get_source_file, 'whatever')
+
+
+class is_standard_module_tc(TestCase):
+ """
+ return true if the module may be considered as a module from the standard
+ library
+ """
+
+ def test_builtins(self):
+ if sys.version_info < (3, 0):
+ self.assertEqual(modutils.is_standard_module('__builtin__'), True)
+ self.assertEqual(modutils.is_standard_module('builtins'), False)
+ else:
+ self.assertEqual(modutils.is_standard_module('__builtin__'), False)
+ self.assertEqual(modutils.is_standard_module('builtins'), True)
+
+ def test_builtin(self):
+ self.assertEqual(modutils.is_standard_module('sys'), True)
+
+ def test_nonstandard(self):
+ self.assertEqual(modutils.is_standard_module('logilab'), False)
+
+ def test_unknown(self):
+ self.assertEqual(modutils.is_standard_module('unknown'), False)
+
+ def test_builtin(self):
+ self.assertEqual(modutils.is_standard_module('marshal'), True)
+
+ def test_4(self):
+ import astroid
+ if sys.version_info > (3, 0):
+ skip = sys.platform.startswith('win') or '.tox' in astroid.__file__
+ if skip:
+ self.skipTest('imp module has a broken behaviour in Python 3 on '
+ 'Windows, returning the module path with different '
+ 'case than it should be.')
+ self.assertEqual(modutils.is_standard_module('hashlib'), True)
+ self.assertEqual(modutils.is_standard_module('pickle'), True)
+ self.assertEqual(modutils.is_standard_module('email'), True)
+ self.assertEqual(modutils.is_standard_module('io'), sys.version_info >= (2, 6))
+ self.assertEqual(modutils.is_standard_module('StringIO'), sys.version_info < (3, 0))
+
+ def test_custom_path(self):
+ if DATADIR.startswith(modutils.EXT_LIB_DIR):
+ self.skipTest('known breakage of is_standard_module on installed package')
+ print repr(DATADIR)
+ print modutils.EXT_LIB_DIR
+ self.assertEqual(modutils.is_standard_module('data.module', (DATADIR,)), True)
+ self.assertEqual(modutils.is_standard_module('data.module', (path.abspath(DATADIR),)), True)
+
+ def test_failing_edge_cases(self):
+ from logilab import common
+ # using a subpackage/submodule path as std_path argument
+ self.assertEqual(modutils.is_standard_module('logilab.common', common.__path__), False)
+ # using a module + object name as modname argument
+ self.assertEqual(modutils.is_standard_module('sys.path'), True)
+ # this is because only the first package/module is considered
+ self.assertEqual(modutils.is_standard_module('sys.whatever'), True)
+ self.assertEqual(modutils.is_standard_module('logilab.whatever', common.__path__), False)
+
+
+class is_relative_tc(TestCase):
+
+
+ def test_knownValues_is_relative_1(self):
+ import astroid
+ self.assertEqual(modutils.is_relative('modutils', astroid.__path__[0]),
+ True)
+
+ def test_knownValues_is_relative_2(self):
+ from logilab.common import tree
+ self.assertEqual(modutils.is_relative('modutils', tree.__file__),
+ True)
+
+ def test_knownValues_is_relative_3(self):
+ import astroid
+ self.assertEqual(modutils.is_relative('astroid', astroid.__path__[0]),
+ False)
+
+
+class get_module_files_tc(TestCase):
+
+ def test_knownValues_get_module_files_1(self): # XXXFIXME: TOWRITE
+ """given a directory return a list of all available python module's files, even
+ in subdirectories
+ """
+ import data
+ modules = sorted(modutils.get_module_files(path.join(DATADIR, 'find_test'),
+ data.__path__[0]))
+ self.assertEqual(modules,
+ [path.join(DATADIR, 'find_test', x) for x in ['__init__.py', 'module.py', 'module2.py', 'noendingnewline.py', 'nonregr.py']])
+
+ def test_load_module_set_attribute(self):
+ import logilab.common.fileutils
+ import logilab
+ del logilab.common.fileutils
+ del sys.modules['logilab.common.fileutils']
+ m = modutils.load_module_from_modpath(['logilab', 'common', 'fileutils'])
+ self.assertTrue( hasattr(logilab, 'common') )
+ self.assertTrue( hasattr(logilab.common, 'fileutils') )
+ self.assertTrue( m is logilab.common.fileutils )
+
+
+from logilab.common.testlib import DocTest
+
+class ModuleDocTest(DocTest):
+ """test doc test in this module"""
+ from astroid import modutils as module
+
+del DocTest # necessary if we don't want it to be executed (we don't...)
+
+
+if __name__ == '__main__':
+ unittest_main()
diff --git a/test/unittest_nodes.py b/test/unittest_nodes.py
index 37300176..d4f164ac 100644
--- a/test/unittest_nodes.py
+++ b/test/unittest_nodes.py
@@ -267,7 +267,7 @@ class ImportNodeTC(testlib.TestCase):
def test_as_string(self):
ast = MODULE['modutils']
- self.assertEqual(ast.as_string(), "from logilab.common import modutils")
+ self.assertEqual(ast.as_string(), "from astroid import modutils")
ast = MODULE['pb']
self.assertEqual(ast.as_string(), "from logilab.common.shellutils import ProgressBar as pb")
ast = MODULE['os']
@@ -313,6 +313,14 @@ except PickleError:
m = astroid['email'].infer(ctx, lookupname='email').next()
self.assertFalse(m.file.startswith(self.datapath('email.py')))
+ def test_more_absolute_import(self):
+ sys.path.insert(0, self.datapath('moreabsimport'))
+ try:
+ astroid = abuilder.file_build(self.datapath('module1abs/__init__.py'))
+ self.assertIn('sys', astroid.locals)
+ finally:
+ sys.path.pop(0)
+
class CmpNodeTC(testlib.TestCase):
def test_as_string(self):
diff --git a/test/unittest_python3.py b/test/unittest_python3.py
index 295984a8..57cea49a 100644
--- a/test/unittest_python3.py
+++ b/test/unittest_python3.py
@@ -1,4 +1,4 @@
-# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of astroid.
@@ -15,12 +15,12 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with astroid. If not, see <http://www.gnu.org/licenses/>.
-import sys
+
from textwrap import dedent
from logilab.common.testlib import TestCase, unittest_main, require_version
-from astroid.node_classes import Assign, Discard, YieldFrom
+from astroid.node_classes import Assign, Discard, YieldFrom, Name, Const
from astroid.manager import AstroidManager
from astroid.builder import AstroidBuilder
from astroid.scoped_nodes import Class, Function
@@ -29,8 +29,8 @@ from astroid.scoped_nodes import Class, Function
class Python3TC(TestCase):
def setUp(self):
self.manager = AstroidManager()
+ self.manager.clear_cache() # take care of borg
self.builder = AstroidBuilder(self.manager)
- self.manager.astroid_cache.clear()
@require_version('3.0')
def test_starred_notation(self):
@@ -97,7 +97,7 @@ class Python3TC(TestCase):
self.assertFalse(klass.metaclass())
@require_version('3.0')
- def test_metaclass_imported(self):
+ def test_metaclass_imported(self):
astroid = self.builder.string_build(dedent("""
from abc import ABCMeta
class Test(metaclass=ABCMeta): pass"""))
@@ -105,7 +105,7 @@ class Python3TC(TestCase):
metaclass = klass.metaclass()
self.assertIsInstance(metaclass, Class)
- self.assertEqual(metaclass.name, 'ABCMeta')
+ self.assertEqual(metaclass.name, 'ABCMeta')
@require_version('3.0')
def test_as_string(self):
@@ -115,7 +115,7 @@ class Python3TC(TestCase):
astroid = self.builder.string_build(body)
klass = astroid.body[1]
- self.assertEqual(klass.as_string(),
+ self.assertEqual(klass.as_string(),
'\n\nclass Test(metaclass=ABCMeta):\n pass\n')
@require_version('3.0')
@@ -180,6 +180,42 @@ class Python3TC(TestCase):
meta = impl.metaclass()
self.assertIsInstance(meta, Class)
self.assertEqual(meta.name, metaclass)
-
+
+ @require_version('3.0')
+ def test_annotation_support(self):
+ astroid = self.builder.string_build(dedent("""
+ def test(a: int, b: str, c: None, d, e,
+ *args: float, **kwargs: int)->int:
+ pass
+ """))
+ func = astroid['test']
+ self.assertIsInstance(func.args.varargannotation, Name)
+ self.assertEqual(func.args.varargannotation.name, 'float')
+ self.assertIsInstance(func.args.kwargannotation, Name)
+ self.assertEqual(func.args.kwargannotation.name, 'int')
+ self.assertIsInstance(func.returns, Name)
+ self.assertEqual(func.returns.name, 'int')
+ arguments = func.args
+ self.assertIsInstance(arguments.annotations[0], Name)
+ self.assertEqual(arguments.annotations[0].name, 'int')
+ self.assertIsInstance(arguments.annotations[1], Name)
+ self.assertEqual(arguments.annotations[1].name, 'str')
+ self.assertIsInstance(arguments.annotations[2], Const)
+ self.assertIsNone(arguments.annotations[2].value)
+ self.assertIsNone(arguments.annotations[3])
+ self.assertIsNone(arguments.annotations[4])
+
+ astroid = self.builder.string_build(dedent("""
+ def test(a: int=1, b: str=2):
+ pass
+ """))
+ func = astroid['test']
+ self.assertIsInstance(func.args.annotations[0], Name)
+ self.assertEqual(func.args.annotations[0].name, 'int')
+ self.assertIsInstance(func.args.annotations[1], Name)
+ self.assertEqual(func.args.annotations[1].name, 'str')
+ self.assertIsNone(func.returns)
+
+
if __name__ == '__main__':
unittest_main()
diff --git a/test/unittest_regrtest.py b/test/unittest_regrtest.py
index d4f937ab..c7bbbd81 100644
--- a/test/unittest_regrtest.py
+++ b/test/unittest_regrtest.py
@@ -1,4 +1,4 @@
-# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of astroid.
@@ -32,6 +32,10 @@ class NonRegressionTC(TestCase):
sys.path.insert(0, join(dirname(abspath(__file__)), 'regrtest_data'))
def tearDown(self):
+ # Since we may have created a brainless manager, leading
+ # to a new cache builtin module and proxy classes in the constants,
+ # clear out the global manager cache.
+ MANAGER.clear_cache()
sys.path.pop(0)
def brainless_manager(self):
@@ -42,6 +46,7 @@ class NonRegressionTC(TestCase):
manager.astroid_cache = {}
manager._mod_file_cache = {}
manager.transforms = {}
+ manager.clear_cache() # trigger proper bootstraping
return manager
def test_module_path(self):
@@ -106,7 +111,7 @@ class A(gobject.GObject):
pylinter = mod['PyLinter']
expect = ['OptionsManagerMixIn', 'object', 'MessagesHandlerMixIn',
'ReportsHandlerMixIn', 'BaseTokenChecker', 'BaseChecker',
- 'OptionsProviderMixIn', 'ASTWalker']
+ 'OptionsProviderMixIn']
self.assertListEqual([c.name for c in pylinter.ancestors()],
expect)
self.assertTrue(list(Instance(pylinter).getattr('config')))
@@ -177,6 +182,24 @@ def run():
# triggers the _is_metaclass call
klass.type
+ def test_decorator_callchain_issue42(self):
+ builder = AstroidBuilder()
+ data = """
+
+def test():
+ def factory(func):
+ def newfunc():
+ func()
+ return newfunc
+ return factory
+
+@test()
+def crash():
+ pass
+"""
+ astroid = builder.string_build(data, __name__, __file__)
+ self.assertEqual(astroid['crash'].type, 'function')
+
class Whatever(object):
a = property(lambda x: x, lambda x: x)
diff --git a/test/unittest_scoped_nodes.py b/test/unittest_scoped_nodes.py
index b6f3434b..94f3a344 100644
--- a/test/unittest_scoped_nodes.py
+++ b/test/unittest_scoped_nodes.py
@@ -411,6 +411,47 @@ test()
self.assertEqual(node.locals['stcmethod'][0].type,
'staticmethod')
+ def test_decorator_builtin_descriptors(self):
+ astroid = abuilder.string_build(dedent("""
+ def static_decorator(platform=None, order=50):
+ def wrapper(f):
+ f.cgm_module = True
+ f.cgm_module_order = order
+ f.cgm_module_platform = platform
+ return staticmethod(f)
+ return wrapper
+
+ def classmethod_decorator(platform=None):
+ def wrapper(f):
+ f.platform = platform
+ return classmethod(f)
+ return wrapper
+
+ class SomeClass(object):
+ @static_decorator()
+ def static(node, cfg):
+ pass
+ @classmethod_decorator()
+ def classmethod(cls):
+ pass
+ @static_decorator
+ def not_so_static(node):
+ pass
+ @classmethod_decorator
+ def not_so_classmethod(node):
+ pass
+
+ """))
+ node = astroid.locals['SomeClass'][0]
+ self.assertEqual(node.locals['static'][0].type,
+ 'staticmethod')
+ self.assertEqual(node.locals['classmethod'][0].type,
+ 'classmethod')
+ self.assertEqual(node.locals['not_so_static'][0].type,
+ 'method')
+ self.assertEqual(node.locals['not_so_classmethod'][0].type,
+ 'method')
+
class ClassNodeTC(TestCase):
@@ -852,6 +893,18 @@ def g2():
self.assertIsInstance(meta, nodes.Class)
self.assertEqual(meta.name, metaclass)
+ def test_metaclass_type(self):
+ klass = extract_node("""
+ def with_metaclass(meta, base=object):
+ return meta("NewBase", (base, ), {})
+
+ class ClassWithMeta(with_metaclass(type)): #@
+ pass
+ """)
+ self.assertEqual(
+ ['NewBase', 'object'],
+ [base.name for base in klass.ancestors()])
+
def test_nonregr_infer_callresult(self):
astroid = abuilder.string_build(dedent("""
class Delegate(object):
@@ -869,6 +922,66 @@ def g2():
# https://bitbucket.org/logilab/astroid/issue/17
self.assertEqual(list(instance.infer()), [YES])
+ def test_slots(self):
+ astroid = abuilder.string_build(dedent("""
+ from collections import deque
+ from textwrap import dedent
+
+ class First(object):
+ __slots__ = ("a", "b", 1)
+ class Second(object):
+ __slots__ = "a"
+ class Third(object):
+ __slots__ = deque(["a", "b", "c"])
+ class Fourth(object):
+ __slots__ = {"a": "a", "b": "b"}
+ class Fifth(object):
+ __slots__ = list
+ class Sixth(object):
+ __slots__ = ""
+ class Seventh(object):
+ __slots__ = dedent.__name__
+ class Eight(object):
+ __slots__ = ("parens")
+ """))
+ first = astroid['First']
+ first_slots = first.slots()
+ self.assertEqual(len(first_slots), 2)
+ self.assertIsInstance(first_slots[0], nodes.Const)
+ self.assertIsInstance(first_slots[1], nodes.Const)
+ self.assertEqual(first_slots[0].value, "a")
+ self.assertEqual(first_slots[1].value, "b")
+
+ second_slots = astroid['Second'].slots()
+ self.assertEqual(len(second_slots), 1)
+ self.assertIsInstance(second_slots[0], nodes.Const)
+ self.assertEqual(second_slots[0].value, "a")
+
+ third_slots = astroid['Third'].slots()
+ self.assertEqual(third_slots, [])
+
+ fourth_slots = astroid['Fourth'].slots()
+ self.assertEqual(len(fourth_slots), 2)
+ self.assertIsInstance(fourth_slots[0], nodes.Const)
+ self.assertIsInstance(fourth_slots[1], nodes.Const)
+ self.assertEqual(fourth_slots[0].value, "a")
+ self.assertEqual(fourth_slots[1].value, "b")
+
+ fifth_slots = astroid['Fifth'].slots()
+ self.assertEqual(fifth_slots, [])
+
+ sixth_slots = astroid['Sixth'].slots()
+ self.assertEqual(sixth_slots, [])
+
+ seventh_slots = astroid['Seventh'].slots()
+ self.assertEqual(len(seventh_slots), 0)
+
+ eight_slots = astroid['Eight'].slots()
+ self.assertEqual(len(eight_slots), 1)
+ self.assertIsInstance(eight_slots[0], nodes.Const)
+ self.assertEqual(eight_slots[0].value, "parens")
+
+
__all__ = ('ModuleNodeTC', 'ImportNodeTC', 'FunctionNodeTC', 'ClassNodeTC')
if __name__ == '__main__':
diff --git a/test_utils.py b/test_utils.py
index 144d19fd..f9f4d53f 100644
--- a/test_utils.py
+++ b/test_utils.py
@@ -27,8 +27,8 @@ def _extract_expressions(node):
expression can be found.
"""
if (isinstance(node, nodes.CallFunc)
- and isinstance(node.func, nodes.Name)
- and node.func.name == _TRANSIENT_FUNCTION):
+ and isinstance(node.func, nodes.Name)
+ and node.func.name == _TRANSIENT_FUNCTION):
real_expr = node.args[0]
real_expr.parent = node.parent
# Search for node in all _astng_fields (the fields checked when
@@ -128,7 +128,7 @@ def extract_node(code, module_name=''):
If no statements or expressions are selected, the last toplevel
statement will be returned.
- If the selected statement is a discard statement, (i.e. an expression
+ If the selected statement is a discard statement, (i.e. an expression
turned into a statement), the wrapped expression is returned instead.
For convenience, singleton lists are unpacked.
diff --git a/utils.py b/utils.py
index 1cd0e778..936c8b5a 100644
--- a/utils.py
+++ b/utils.py
@@ -25,7 +25,7 @@ from astroid.exceptions import AstroidBuildingException
from astroid.builder import parse
-class ASTWalker:
+class ASTWalker(object):
"""a walker visiting a tree in preorder, calling on the handler:
* visit_<class name> on entering a node, where class name is the class of
@@ -98,7 +98,7 @@ class LocalsVisitor(ASTWalker):
if methods[0] is not None:
methods[0](node)
if 'locals' in node.__dict__: # skip Instance and other proxy
- for name, local_node in node.items():
+ for local_node in node.values():
self.visit(local_node)
if methods[1] is not None:
return methods[1](node)
@@ -112,12 +112,14 @@ def _check_children(node):
print "Hm, child of %s is None" % node
continue
if not hasattr(child, 'parent'):
- print " ERROR: %s has child %s %x with no parent" % (node, child, id(child))
+ print " ERROR: %s has child %s %x with no parent" % (
+ node, child, id(child))
elif not child.parent:
- print " ERROR: %s has child %s %x with parent %r" % (node, child, id(child), child.parent)
+ print " ERROR: %s has child %s %x with parent %r" % (
+ node, child, id(child), child.parent)
elif child.parent is not node:
- print " ERROR: %s %x has child %s %x with wrong parent %s" % (node,
- id(node), child, id(child), child.parent)
+ print " ERROR: %s %x has child %s %x with wrong parent %s" % (
+ node, id(node), child, id(child), child.parent)
else:
ok = True
if not ok:
@@ -145,7 +147,7 @@ class TreeTester(object):
Module()
body = [
Print()
- dest =
+ dest =
values = [
]
]
@@ -180,8 +182,8 @@ class TreeTester(object):
if _done is None:
_done = set()
if node in _done:
- self._string += '\nloop in tree: %r (%s)' % (node,
- getattr(node, 'lineno', None))
+ self._string += '\nloop in tree: %r (%s)' % (
+ node, getattr(node, 'lineno', None))
return
_done.add(node)
self._string += '\n' + indent + '<%s>' % node.__class__.__name__
@@ -197,7 +199,7 @@ class TreeTester(object):
continue
if a in ("lineno", "col_offset") and not self.lineno:
continue
- self._string +='\n' + indent + a + " = " + repr(attr)
+ self._string += '\n' + indent + a + " = " + repr(attr)
for field in node._fields or ():
attr = node_dict[field]
if attr is None: