# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
"""This module extends ast "scoped" node, i.e. which are opening a new
local scope in the language definition : Module, Class, Function (and
Lambda in some extends).
Each new methods and attributes added on each class are documented
below.
:author: Sylvain Thenault
:copyright: 2003-2007 LOGILAB S.A. (Paris, FRANCE)
:contact: http://www.logilab.fr/ -- mailto:python-projects@logilab.org
:copyright: 2003-2007 Sylvain Thenault
:contact: mailto:thenault@gmail.com
"""
from __future__ import generators
__doctype__ = "restructuredtext en"
import sys
from logilab.common.compat import chain, set
from logilab.astng.utils import extend_class
from logilab.astng import YES, MANAGER, Instance, InferenceContext, copy_context, \
unpack_infer, _infer_stmts, \
Class, Const, Dict, Function, GenExpr, Lambda, \
Module, Name, Pass, Raise, Tuple, Yield
from logilab.astng import NotFoundError, NoDefault, \
ASTNGBuildingException, InferenceError
# module class dict/iterator interface ########################################
class LocalsDictMixIn(object):
""" this class provides locals handling common to Module, Function
and Class nodes, including a dict like interface for direct access
to locals information
/!\ this class should not be used directly /!\ it's
only used as a methods and attribute container, and update the
original class from the compiler.ast module using its dictionnary
(see below the class definition)
"""
# attributes below are set by the builder module or by raw factories
# dictionary of locals with name as key and node defining the local as
# value
locals = None
def qname(self):
"""return the 'qualified' name of the node, eg module.name,
module.class.name ...
"""
if self.parent is None:
return self.name
return '%s.%s' % (self.parent.frame().qname(), self.name)
def frame(self):
"""return the first parent frame node (i.e. Module, Function or Class)
"""
return self
def scope(self):
"""return the first node defining a new scope (i.e. Module,
Function, Class, Lambda but also GenExpr)
"""
return self
def set_local(self, name, stmt):
"""define <name> in locals (<stmt> is the node defining the name)
if the node is a Module node (i.e. has globals), add the name to
globals
if the name is already defined, ignore it
"""
self.locals.setdefault(name, []).append(stmt)
__setitem__ = set_local
def add_local_node(self, child_node, name=None):
"""append a child which should alter locals to the given node"""
self._append_node(child_node)
self.set_local(name or child_node.name, child_node)
def _append_node(self, child_node):
"""append a child, linking it in the tree"""
self.code.nodes.append(child_node)
child_node.parent = self
def __getitem__(self, item):
"""method from the `dict` interface returning the first node
associated with the given name in the locals dictionnary
:type item: str
:param item: the name of the locally defined object
:raises KeyError: if the name is not defined
"""
return self.locals[item][0]
def __iter__(self):
"""method from the `dict` interface returning an iterator on
`self.keys()`
"""
return iter(self.keys())
def keys(self):
"""method from the `dict` interface returning a tuple containing
locally defined names
"""
return self.locals.keys()
## associated to nodes which are instance of `Function` or
## `Class`
## """
## # FIXME: sort keys according to line number ?
## try:
## return self.__keys
## except AttributeError:
## keys = [member.name for member in self.locals.values()
## if (isinstance(member, Function)
## or isinstance(member, Class))
## and member.parent.frame() is self]
## self.__keys = tuple(keys)
## return keys
def values(self):
"""method from the `dict` interface returning a tuple containing
locally defined nodes which are instance of `Function` or `Class`
"""
return [self[key] for key in self.keys()]
def items(self):
"""method from the `dict` interface returning a list of tuple
containing each locally defined name with its associated node,
which is an instance of `Function` or `Class`
"""
return zip(self.keys(), self.values())
def has_key(self, name):
"""method from the `dict` interface returning True if the given
name is defined in the locals dictionary
"""
return self.locals.has_key(name)
__contains__ = has_key
extend_class(Module, LocalsDictMixIn)
extend_class(Class, LocalsDictMixIn)
extend_class(Function, LocalsDictMixIn)
extend_class(Lambda, LocalsDictMixIn)
# GenExpr has it's own locals but isn't a frame
extend_class(GenExpr, LocalsDictMixIn)
def frame(self):
return self.parent.frame()
GenExpr.frame = frame
class GetattrMixIn(object):
def getattr(self, name, context=None):
try:
return self.locals[name]
except KeyError:
raise NotFoundError(name)
def igetattr(self, name, context=None):
"""infered getattr"""
# set lookup name since this is necessary to infer on import nodes for
# instance
context = copy_context(context)
context.lookupname = name
try:
return _infer_stmts(self.getattr(name, context), context, frame=self)
except NotFoundError:
raise InferenceError(name)
extend_class(Module, GetattrMixIn)
extend_class(Class, GetattrMixIn)
# Module #####################################################################
class ModuleNG(object):
"""/!\ this class should not be used directly /!\ it's
only used as a methods and attribute container, and update the
original class from the compiler.ast module using its dictionnary
(see below the class definition)
"""
# attributes below are set by the builder module or by raw factories
# the file from which as been extracted the astng representation. It may
# be None if the representation has been built from a built-in module
file = None
# the module name
name = None
# boolean for astng built from source (i.e. ast)
pure_python = None
# boolean for package module
package = None
# dictionary of globals with name as key and node defining the global
# as value
globals = None
def pytype(self):
return '__builtin__.module'
def getattr(self, name, context=None):
try:
return self.locals[name]
except KeyError:
if self.package:
try:
return [self.import_module(name, relative_only=True)]
except KeyboardInterrupt:
raise
except:
pass
raise NotFoundError(name)
def _append_node(self, child_node):
"""append a child version specific to Module node"""
self.node.nodes.append(child_node)
child_node.parent = self
def source_line(self):
"""return the source line number, 0 on a module"""
return 0
def fully_defined(self):
"""return True if this module has been built from a .py file
and so contains a complete representation including the code
"""
return self.file is not None and self.file.endswith('.py')
def statement(self):
"""return the first parent node marked as statement node
consider a module as a statement...
"""
return self
def import_module(self, modname, relative_only=False):
"""import the given module considering self as context"""
try:
return MANAGER.astng_from_module_name(self.relative_name(modname))
except ASTNGBuildingException:
if relative_only:
raise
module = MANAGER.astng_from_module_name(modname)
return module
def relative_name(self, modname):
if self.package:
return '%s.%s' % (self.name, modname)
package_name = '.'.join(self.name.split('.')[:-1])
if package_name:
return '%s.%s' % (package_name, modname)
return modname
def wildcard_import_names(self):
"""return the list of imported names when this module is 'wildard
imported'
It doesn't include the '__builtins__' name which is added by the
current CPython implementation of wildcard imports.
"""
# take advantage of a living module if it exists
try:
living = sys.modules[self.name]
except KeyError:
pass
else:
try:
return living.__all__
except AttributeError:
return [name for name in living.__dict__.keys()
if not name.startswith('_')]
# else lookup the astng
try:
explicit = self['__all__'].assigned_stmts().next()
# should be a tuple of constant string
return [const.value for const in explicit.nodes]
except (KeyError, AttributeError, InferenceError):
# XXX should admit we have lost if there is something like
# __all__ that we've not been able to analyse (such as
# dynamically constructed __all__)
return [name for name in self.keys()
if not name.startswith('_')]
extend_class(Module, ModuleNG)
# Function ###################################################################
class FunctionNG(object):
"""/!\ this class should not be used directly /!\ it's
only used as a methods and attribute container, and update the
original class from the compiler.ast module using its dictionnary
(see below the class definition)
"""
# attributes below are set by the builder module or by raw factories
# function's type, 'function' | 'method' | 'staticmethod' | 'classmethod'
type = 'function'
# list of argument names. MAY BE NONE on some builtin functions where
# arguments are unknown
argnames = None
def pytype(self):
if 'method' in self.type:
return '__builtin__.instancemethod'
return '__builtin__.function'
def is_method(self):
"""return true if the function node should be considered as a method"""
return self.type != 'function'
def is_abstract(self, pass_is_abstract=True):
"""return true if the method is abstract
It's considered as abstract if the only statement is a raise of
NotImplementError, or, if pass_is_abstract, a pass statement
"""
for child_node in self.code.getChildNodes():
if isinstance(child_node, Raise) and child_node.expr1:
try:
name = child_node.expr1.nodes_of_class(Name).next()
if name.name == 'NotImplementedError':
return True
except StopIteration:
pass
if pass_is_abstract and isinstance(child_node, Pass):
return True
return False
# empty function is the same as function with a single "pass" statement
if pass_is_abstract:
return True
def is_generator(self):
"""return true if this is a generator function"""
try:
return self.nodes_of_class(Yield, skip_klass=Function).next()
except StopIteration:
return False
def format_args(self):
"""return arguments formatted as string"""
if self.argnames is None: # information is missing
return ''
result = []
args, kwargs, last, default_idx = self._pos_information()
for i in range(len(self.argnames)):
name = self.argnames[i]
if type(name) is type(()):
name = '(%s)' % ','.join(name)
if i == last and kwargs:
name = '**%s' % name
elif args and i == last or (kwargs and i == last - 1):
name = '*%s' % name
elif i >= default_idx:
default_str = self.defaults[i - default_idx].as_string()
name = '%s=%s' % (name, default_str)
result.append(name)
return ', '.join(result)
def default_value(self, argname):
"""return the default value for an argument
:raise `NoDefault`: if there is no default value defined
"""
if self.argnames is None: # information is missing
raise NoDefault()
args, kwargs, last, defaultidx = self._pos_information()
i = self.argnames.index(argname)
if i >= defaultidx and (i - defaultidx) < len(self.defaults):
return self.defaults[i - defaultidx]
raise NoDefault()
def mularg_class(self, argname):
"""if the given argument is a * or ** argument, return respectivly
a Tuple or Dict instance, else return None
"""
args, kwargs, last, defaultidx = self._pos_information()
i = self.argnames.index(argname)
if i == last and kwargs:
valnode = Dict([])
valnode.parent = self
return valnode
if args and (i == last or (kwargs and i == last - 1)):
valnode = Tuple([])
valnode.parent = self
return valnode
return None
def _pos_information(self):
"""return a 4-uple with positional information about arguments:
(true if * is used,
true if ** is used,
index of the last argument,
index of the first argument having a default value)
"""
args = self.flags & 4
kwargs = self.flags & 8
last = len(self.argnames) - 1
defaultidx = len(self.argnames) - (len(self.defaults) +
(args and 1 or 0) +
(kwargs and 1 or 0))
return args, kwargs, last, defaultidx
extend_class(Function, FunctionNG)
# lambda nodes may also need some of the function members
Lambda._pos_information = FunctionNG._pos_information.im_func
Lambda.format_args = FunctionNG.format_args.im_func
Lambda.default_value = FunctionNG.default_value.im_func
Lambda.mularg_class = FunctionNG.mularg_class.im_func
Lambda.type = 'function'
# Class ######################################################################
def _class_type(klass):
"""return a Class node type to differ metaclass, interface and exception
from 'regular' classes
"""
if klass._type is not None:
return klass._type
if klass.name == 'type':
klass._type = 'metaclass'
elif klass.name.endswith('Interface'):
klass._type = 'interface'
elif klass.name.endswith('Exception'):
klass._type = 'exception'
else:
for base in klass.ancestors(recurs=False):
if base.type != 'class':
klass._type = base.type
break
if klass._type is None:
klass._type = 'class'
return klass._type
def _iface_hdlr(iface_node):
"""a handler function used by interfaces to handle suspicious
interface nodes
"""
return True
class ClassNG(object):
"""/!\ this class should not be used directly /!\ it's
only used as a methods and attribute container, and update the
original class from the compiler.ast module using its dictionnary
(see below the class definition)
"""
_type = None
type = property(_class_type,
doc="class'type, possible values are 'class' | "
"'metaclass' | 'interface' | 'exception'")
def _newstyle_impl(self, context=None):
context = context or InferenceContext()
if self._newstyle is not None:
return self._newstyle
for base in self.ancestors(recurs=False, context=context):
if base._newstyle_impl(context):
self._newstyle = True
break
if self._newstyle is None:
self._newstyle = False
return self._newstyle
_newstyle = None
newstyle = property(_newstyle_impl,
doc="boolean indicating if it's a new style class"
"or not")
def pytype(self):
if self.newstyle:
return '__builtin__.type'
return '__builtin__.classobj'
# attributes below are set by the builder module or by raw factories
# a dictionary of class instances attributes
instance_attrs = None
# list of parent class as a list of string (ie names as they appears
# in the class definition)
basenames = None
def ancestors(self, recurs=True, context=None):
"""return an iterator on the node base classes in a prefixed
depth first order
:param recurs:
boolean indicating if it should recurse or return direct
ancestors only
"""
# FIXME: should be possible to choose the resolution order
# XXX inference make infinite loops possible here (see BaseTransformer
# manipulation in the builder module for instance !)
context = context or InferenceContext()
for stmt in self.bases:
try:
for baseobj in stmt.infer(context):
if not isinstance(baseobj, Class):
# duh ?
continue
if baseobj is self:
continue # cf xxx above
yield baseobj
if recurs:
for grandpa in baseobj.ancestors(True, context):
if grandpa is self:
continue # cf xxx above
yield grandpa
except InferenceError:
#import traceback
#traceback.print_exc()
# XXX log error ?
continue
def local_attr_ancestors(self, name, context=None):
"""return an iterator on astng representation of parent classes
which have <name> defined in their locals
"""
for astng in self.ancestors(context=context):
if astng.locals.has_key(name):
yield astng
def instance_attr_ancestors(self, name, context=None):
"""return an iterator on astng representation of parent classes
which have <name> defined in their instance attribute dictionary
"""
for astng in self.ancestors(context=context):
if astng.instance_attrs.has_key(name):
yield astng
def local_attr(self, name, context=None):
"""return the astng associated to name in this class locals or
in its parents
:raises `NotFoundError`:
if no attribute with this name has been find in this class or
its parent classes
"""
try:
return self[name]
except KeyError:
# get if from the first parent implementing it if any
for class_node in self.local_attr_ancestors(name, context):
return class_node[name]
raise NotFoundError(name)
def instance_attr(self, name, context=None):
"""return the astng nodes associated to name in this class instance
attributes dictionary or in its parents
:raises `NotFoundError`:
if no attribute with this name has been find in this class or
its parent classes
"""
try:
return self.instance_attrs[name]
except KeyError:
# get if from the first parent implementing it if any
for class_node in self.instance_attr_ancestors(name, context):
return class_node.instance_attrs[name]
raise NotFoundError(name)
def getattr(self, name, context=None):
"""this method doesn't look in the instance_attrs dictionary since it's
done by an Instance proxy at inference time.
It may return a YES object if the attribute has not been actually
found but a __getattr__ or __getattribute__ method is defined
"""
if name in self.locals:
return self.locals[name]
if name == '__bases__':
return tuple(self.ancestors(recurs=False))
# XXX need proper meta class handling + MRO implementation
if name == '__mro__':
return tuple(self.ancestors(recurs=True))
for classnode in self.ancestors(recurs=False, context=context):
try:
return classnode.getattr(name, context)
except NotFoundError:
continue
raise NotFoundError(name)
def igetattr(self, name, context=None):
"""infered getattr, need special treatment in class to handle
descriptors
"""
# set lookoup name since this is necessary to infer on import nodes for
# instance
context = copy_context(context)
context.lookupname = name
try:
for infered in _infer_stmts(self.getattr(name, context), context,
frame=self):
# yield YES object instead of descriptors when necessary
if not isinstance(infered, Const) and isinstance(infered, Instance):
try:
infered._proxied.getattr('__get__', context)
except NotFoundError:
yield infered
else:
yield YES
else:
yield infered
except NotFoundError:
if not name.startswith('__') and self.has_dynamic_getattr(context):
# class handle some dynamic attributes, return a YES object
yield YES
else:
raise InferenceError(name)
def has_dynamic_getattr(self, context=None):
"""return True if the class has a custom __getattr__ or
__getattribute__ method
"""
# need to explicitly handle optparse.Values (setattr is not detected)
if self.name == 'Values' and self.root().name == 'optparse':
return True
try:
self.getattr('__getattr__', context)
return True
except NotFoundError:
#if self.newstyle: XXX cause an infinite recursion error
try:
getattribute = self.getattr('__getattribute__', context)[0]
if getattribute.root().name != '__builtin__':
# class has a custom __getattribute__ defined
return True
except NotFoundError:
pass
return False
def methods(self):
"""return an iterator on all methods defined in the class and
its ancestors
"""
done = {}
for astng in chain(iter((self,)), self.ancestors()):
for meth in astng.mymethods():
if done.has_key(meth.name):
continue
done[meth.name] = None
yield meth
def mymethods(self):
"""return an iterator on all methods defined in the class"""
for member in self.values():
if isinstance(member, Function):
yield member
def interfaces(self, herited=True, handler_func=_iface_hdlr):
"""return an iterator on interfaces implemented by the given
class node
"""
# FIXME: what if __implements__ = (MyIFace, MyParent.__implements__)...
try:
implements = Instance(self).getattr('__implements__')[0]
except NotFoundError:
return
if not herited and not implements.frame() is self:
return
oneinf = False
for iface in unpack_infer(implements):
if iface is YES:
continue
if handler_func(iface):
oneinf = True
yield iface
if not oneinf:
raise InferenceError()
## if hasattr(implements, 'nodes'):
## implements = implements.nodes
## else:
## implements = (implements,)
## for iface in implements:
## # let the handler function take care of this....
## for iface in handler_func(iface):
## yield iface
extend_class(Class, ClassNG)
syntax highlighted by Code2HTML, v. 0.9.1