354 lines
11 KiB
Python
354 lines
11 KiB
Python
# -*- coding:utf-8 -*-
|
|
#
|
|
# Copyright 2014 Hewlett-Packard Development Company, L.P.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
import _ast
|
|
import ast
|
|
import os.path
|
|
import symtable
|
|
|
|
|
|
"""Various helper functions."""
|
|
|
|
|
|
def ast_args_to_str(args):
|
|
res = ('\n\tArgument/s:\n\t\t%s' %
|
|
'\n\t\t'.join([ast.dump(arg) for arg in args]))
|
|
return res
|
|
|
|
|
|
def _get_attr_qual_name(node, aliases):
|
|
'''Get a the full name for the attribute node.
|
|
|
|
This will resolve a pseudo-qualified name for the attribute
|
|
rooted at node as long as all the deeper nodes are Names or
|
|
Attributes. This will give you how the code referenced the name but
|
|
will not tell you what the name actually refers to. If we
|
|
encounter a node without a static name we punt with an
|
|
empty string. If this encounters something more comples, such as
|
|
foo.mylist[0](a,b) we just return empty string.
|
|
|
|
:param node: AST Name or Attribute node
|
|
:param aliases: Import aliases dictionary
|
|
:returns: Qualified name refered to by the attribute or name.
|
|
'''
|
|
if type(node) == _ast.Name:
|
|
if node.id in aliases:
|
|
return aliases[node.id]
|
|
return node.id
|
|
elif type(node) == _ast.Attribute:
|
|
name = '%s.%s' % (_get_attr_qual_name(node.value, aliases), node.attr)
|
|
if name in aliases:
|
|
return aliases[name]
|
|
return name
|
|
else:
|
|
return ""
|
|
|
|
|
|
def get_call_name(node, aliases):
|
|
if type(node.func) == _ast.Name:
|
|
if deepgetattr(node, 'func.id') in aliases:
|
|
return aliases[deepgetattr(node, 'func.id')]
|
|
return(deepgetattr(node, 'func.id'))
|
|
elif type(node.func) == _ast.Attribute:
|
|
return _get_attr_qual_name(node.func, aliases)
|
|
else:
|
|
return ""
|
|
|
|
|
|
def get_func_name(node):
|
|
return node.name # TODO(tkelsey): get that qualname using enclosing scope
|
|
|
|
|
|
def get_qual_attr(node, aliases):
|
|
prefix = ""
|
|
if type(node) == _ast.Attribute:
|
|
try:
|
|
val = deepgetattr(node, 'value.id')
|
|
if val in aliases:
|
|
prefix = aliases[val]
|
|
else:
|
|
prefix = deepgetattr(node, 'value.id')
|
|
except Exception:
|
|
# NOTE(tkelsey): degrade gracefully when we cant get the fully
|
|
# qualified name for an attr, just return its base name.
|
|
pass
|
|
|
|
return("%s.%s" % (prefix, node.attr))
|
|
else:
|
|
return "" # TODO(tkelsey): process other node types
|
|
|
|
|
|
def deepgetattr(obj, attr):
|
|
"""Recurses through an attribute chain to get the ultimate value."""
|
|
for key in attr.split('.'):
|
|
obj = getattr(obj, key)
|
|
return obj
|
|
|
|
|
|
def describe_symbol(sym):
|
|
assert type(sym) == symtable.Symbol
|
|
print("Symbol:", sym.get_name())
|
|
|
|
for prop in [
|
|
'referenced', 'imported', 'parameter',
|
|
'global', 'declared_global', 'local',
|
|
'free', 'assigned', 'namespace']:
|
|
if getattr(sym, 'is_' + prop)():
|
|
print(' is', prop)
|
|
|
|
|
|
def lines_with_context(line_no, line_range, max_lines, file_len):
|
|
'''Get affected lines, plus context
|
|
|
|
This function takes a list of line numbers, adds one line
|
|
before the specified range, and two lines after, to provide
|
|
a bit more context. It then limits the number of lines to
|
|
the specified max_lines value.
|
|
:param line_no: The line of interest (trigger line)
|
|
:param line_range: The lines that make up the whole statement
|
|
:param max_lines: The maximum number of lines to output
|
|
:return l_range: A list of line numbers to output
|
|
'''
|
|
|
|
# Catch a 0 or negative max lines, don't display any code
|
|
if max_lines == 0:
|
|
return []
|
|
|
|
l_range = sorted(line_range)
|
|
|
|
# add one line before before and after, to make sure we don't miss
|
|
# any context.
|
|
l_range.append(l_range[-1] + 1)
|
|
l_range.append(l_range[0] - 1)
|
|
|
|
l_range = sorted(l_range)
|
|
|
|
if max_lines < 0:
|
|
return l_range
|
|
|
|
# limit scope to max_lines
|
|
if len(l_range) > max_lines:
|
|
# figure out a sane distribution of scope (extra lines after)
|
|
after = (max_lines - 1) / 2
|
|
before = max_lines - (after + 1)
|
|
target = l_range.index(line_no)
|
|
|
|
# skew things if the code is at the start or end of the statement
|
|
|
|
if before > target:
|
|
extra = before - target
|
|
before = target
|
|
after += extra
|
|
|
|
gap = file_len - (target + 1)
|
|
if gap < after:
|
|
extra = after - gap
|
|
after = gap
|
|
before += extra
|
|
|
|
# find start
|
|
if before >= target:
|
|
start = 0
|
|
else:
|
|
start = target - before
|
|
|
|
# find end
|
|
if target + after > len(l_range) - 1:
|
|
end = len(l_range) - 1
|
|
else:
|
|
end = target + after
|
|
|
|
# slice line array
|
|
l_range = l_range[start:end + 1]
|
|
|
|
return l_range
|
|
|
|
|
|
class InvalidModulePath(Exception):
|
|
pass
|
|
|
|
|
|
class NoConfigFileFound(Exception):
|
|
def __init__(self, config_locations):
|
|
message = ("no config found - tried: " +
|
|
", ".join(config_locations))
|
|
super(NoConfigFileFound, self).__init__(message)
|
|
|
|
|
|
def get_module_qualname_from_path(path):
|
|
'''Get the module's qualified name by analysis of the path.
|
|
|
|
Resolve the absolute pathname and eliminate symlinks. This could result in
|
|
an incorrect name if symlinks are used to restructure the python lib
|
|
directory.
|
|
|
|
Starting from the right-most directory component look for __init__.py in
|
|
the directory component. If it exists then the directory name is part of
|
|
the module name. Move left to the subsequent directory components until a
|
|
directory is found without __init__.py.
|
|
|
|
:param: Path to module file. Relative paths will be resolved relative to
|
|
current working directory.
|
|
:return: fully qualified module name
|
|
'''
|
|
|
|
(head, tail) = os.path.split(path)
|
|
if head == '' or tail == '':
|
|
raise InvalidModulePath('Invalid python file path: "%s"'
|
|
' Missing path or file name' % (path))
|
|
|
|
qname = [os.path.splitext(tail)[0]]
|
|
while head != '/':
|
|
if os.path.isfile(os.path.join(head, '__init__.py')):
|
|
(head, tail) = os.path.split(head)
|
|
qname.insert(0, tail)
|
|
else:
|
|
break
|
|
|
|
qualname = '.'.join(qname)
|
|
return qualname
|
|
|
|
|
|
def namespace_path_join(base, name):
|
|
'''Extend the current namespace path with an additional name
|
|
|
|
Take a namespace path (i.e., package.module.class) and extends it
|
|
with an additional name (i.e., package.module.class.subclass).
|
|
This is similar to how os.path.join works.
|
|
|
|
:param base: (String) The base namespace path.
|
|
:param name: (String) The new name to append to the base path.
|
|
:returns: (String) A new namespace path resulting from combination of
|
|
base and name.
|
|
'''
|
|
return '%s.%s' % (base, name)
|
|
|
|
|
|
def namespace_path_split(path):
|
|
'''Split the namespace path into a pair (head, tail).
|
|
|
|
Tail will be the last namespace path component and head will
|
|
be everything leading up to that in the path. This is similar to
|
|
os.path.split.
|
|
|
|
:param path: (String) A namespace path.
|
|
:returns: (String, String) A tuple where the first component is the base
|
|
path and the second is the last path component.
|
|
'''
|
|
return tuple(path.rsplit('.', 1))
|
|
|
|
|
|
def safe_unicode(obj, *args):
|
|
'''return the unicode representation of obj.'''
|
|
try:
|
|
return unicode(obj, *args)
|
|
except UnicodeDecodeError:
|
|
# obj is byte string
|
|
ascii_text = str(obj).encode('string_escape')
|
|
return unicode(ascii_text)
|
|
|
|
|
|
def safe_str(obj):
|
|
'''return the byte string representation of obj.'''
|
|
try:
|
|
return str(obj)
|
|
except UnicodeEncodeError:
|
|
# obj is unicode
|
|
return unicode(obj).encode('unicode_escape')
|
|
|
|
|
|
def linerange(node):
|
|
"""Get line number range from a node."""
|
|
strip = {"body": None, "orelse": None,
|
|
"handlers": None, "finalbody": None}
|
|
fields = dir(node)
|
|
for key in strip.keys():
|
|
if key in fields:
|
|
strip[key] = getattr(node, key)
|
|
setattr(node, key, [])
|
|
|
|
lines = set()
|
|
for n in ast.walk(node):
|
|
if hasattr(n, 'lineno'):
|
|
lines.add(n.lineno)
|
|
|
|
for key in strip.keys():
|
|
if strip[key] is not None:
|
|
setattr(node, key, strip[key])
|
|
|
|
if len(lines):
|
|
return range(min(lines), max(lines) + 1)
|
|
return [0, 1]
|
|
|
|
|
|
def linerange_fix(node):
|
|
"""Try and work around a known Python bug with multi-line strings."""
|
|
# deal with multiline strings lineno behavior (Python issue #16806)
|
|
lines = linerange(node)
|
|
if hasattr(node, 'sibling') and hasattr(node.sibling, 'lineno'):
|
|
start = min(lines)
|
|
delta = node.sibling.lineno - start
|
|
if delta > 1:
|
|
return range(start, node.sibling.lineno)
|
|
return lines
|
|
|
|
|
|
def concat_string(node, stop=None):
|
|
'''Builds a string from a ast.BinOp chain.
|
|
|
|
This will build a string from a series of ast.Str nodes wrapped in
|
|
ast.BinOp nodes. Somthing like "a" + "b" + "c" or "a %s" % val etc.
|
|
The provided node can be any participant in the BinOp chain.
|
|
|
|
:param node: (ast.Str or ast.BinOp) The node to process
|
|
:param stop: (ast.Str or ast.BinOp) Optional base node to stop at
|
|
:returns: (Tuple) the root node of the expression, the string value
|
|
'''
|
|
def _get(node, bits, stop=None):
|
|
if node != stop:
|
|
bits.append(
|
|
_get(node.left, bits, stop)
|
|
if isinstance(node.left, ast.BinOp)
|
|
else node.left)
|
|
bits.append(
|
|
_get(node.right, bits, stop)
|
|
if isinstance(node.right, ast.BinOp)
|
|
else node.right)
|
|
|
|
bits = [node]
|
|
while isinstance(node.parent, ast.BinOp):
|
|
node = node.parent
|
|
if isinstance(node, ast.BinOp):
|
|
_get(node, bits, stop)
|
|
return (node, " ".join([x.s for x in bits if isinstance(x, ast.Str)]))
|
|
|
|
|
|
def get_called_name(node):
|
|
'''Get a function name from an ast.Call node.
|
|
|
|
An ast.Call node representing a method call with present differently to one
|
|
wrapping a function call: thing.call() vs call(). This helper will grab the
|
|
unqualified call name correctly in either case.
|
|
|
|
:param node: (ast.Call) the call node
|
|
:returns: (String) the function name
|
|
'''
|
|
func = node.func
|
|
try:
|
|
return (func.attr if isinstance(func, ast.Attribute) else func.id)
|
|
except AttributeError:
|
|
return ""
|