diff options
| author | Eevee (Alex Munroe) <amunroe@yelp.com> | 2014-07-31 16:32:21 -0700 |
|---|---|---|
| committer | Eevee (Alex Munroe) <amunroe@yelp.com> | 2014-07-31 16:32:21 -0700 |
| commit | 3a090e2819c85abacae5dd244733408cb110e427 (patch) | |
| tree | 22afd25a9e0b62afb2c456c8f63fd2b69d597f02 | |
| parent | 58e5e9774ba6a5cb378f2e30eb026489d66922fd (diff) | |
| parent | bf919348a823a573ebbb7c435626172ba21b7c3c (diff) | |
| download | astroid-git-3a090e2819c85abacae5dd244733408cb110e427.tar.gz | |
merged with default
37 files changed, 1727 insertions, 258 deletions
@@ -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 @@ -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: @@ -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) @@ -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): @@ -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""" @@ -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. @@ -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: |
