|
import re |
|
import textwrap |
|
from ast import literal_eval |
|
from inspect import cleandoc |
|
from weakref import WeakKeyDictionary |
|
|
|
from parso.python import tree |
|
from parso.cache import parser_cache |
|
from parso import split_lines |
|
|
|
_EXECUTE_NODES = {'funcdef', 'classdef', 'import_from', 'import_name', 'test', |
|
'or_test', 'and_test', 'not_test', 'comparison', 'expr', |
|
'xor_expr', 'and_expr', 'shift_expr', 'arith_expr', |
|
'atom_expr', 'term', 'factor', 'power', 'atom'} |
|
|
|
_FLOW_KEYWORDS = ( |
|
'try', 'except', 'finally', 'else', 'if', 'elif', 'with', 'for', 'while' |
|
) |
|
|
|
|
|
def get_executable_nodes(node, last_added=False): |
|
""" |
|
For static analysis. |
|
""" |
|
result = [] |
|
typ = node.type |
|
if typ == 'name': |
|
next_leaf = node.get_next_leaf() |
|
if last_added is False and node.parent.type != 'param' and next_leaf != '=': |
|
result.append(node) |
|
elif typ == 'expr_stmt': |
|
|
|
|
|
result.append(node) |
|
for child in node.children: |
|
result += get_executable_nodes(child, last_added=True) |
|
elif typ == 'decorator': |
|
|
|
if node.children[-2] == ')': |
|
node = node.children[-3] |
|
if node != '(': |
|
result += get_executable_nodes(node) |
|
else: |
|
try: |
|
children = node.children |
|
except AttributeError: |
|
pass |
|
else: |
|
if node.type in _EXECUTE_NODES and not last_added: |
|
result.append(node) |
|
|
|
for child in children: |
|
result += get_executable_nodes(child, last_added) |
|
|
|
return result |
|
|
|
|
|
def get_sync_comp_fors(comp_for): |
|
yield comp_for |
|
last = comp_for.children[-1] |
|
while True: |
|
if last.type == 'comp_for': |
|
yield last.children[1] |
|
elif last.type == 'sync_comp_for': |
|
yield last |
|
elif not last.type == 'comp_if': |
|
break |
|
last = last.children[-1] |
|
|
|
|
|
def for_stmt_defines_one_name(for_stmt): |
|
""" |
|
Returns True if only one name is returned: ``for x in y``. |
|
Returns False if the for loop is more complicated: ``for x, z in y``. |
|
|
|
:returns: bool |
|
""" |
|
return for_stmt.children[1].type == 'name' |
|
|
|
|
|
def get_flow_branch_keyword(flow_node, node): |
|
start_pos = node.start_pos |
|
if not (flow_node.start_pos < start_pos <= flow_node.end_pos): |
|
raise ValueError('The node is not part of the flow.') |
|
|
|
keyword = None |
|
for i, child in enumerate(flow_node.children): |
|
if start_pos < child.start_pos: |
|
return keyword |
|
first_leaf = child.get_first_leaf() |
|
if first_leaf in _FLOW_KEYWORDS: |
|
keyword = first_leaf |
|
return None |
|
|
|
|
|
def clean_scope_docstring(scope_node): |
|
""" Returns a cleaned version of the docstring token. """ |
|
node = scope_node.get_doc_node() |
|
if node is not None: |
|
|
|
|
|
|
|
|
|
return cleandoc(safe_literal_eval(node.value)) |
|
return '' |
|
|
|
|
|
def find_statement_documentation(tree_node): |
|
if tree_node.type == 'expr_stmt': |
|
tree_node = tree_node.parent |
|
maybe_string = tree_node.get_next_sibling() |
|
if maybe_string is not None: |
|
if maybe_string.type == 'simple_stmt': |
|
maybe_string = maybe_string.children[0] |
|
if maybe_string.type == 'string': |
|
return cleandoc(safe_literal_eval(maybe_string.value)) |
|
return '' |
|
|
|
|
|
def safe_literal_eval(value): |
|
first_two = value[:2].lower() |
|
if first_two[0] == 'f' or first_two in ('fr', 'rf'): |
|
|
|
|
|
return '' |
|
|
|
return literal_eval(value) |
|
|
|
|
|
def get_signature(funcdef, width=72, call_string=None, |
|
omit_first_param=False, omit_return_annotation=False): |
|
""" |
|
Generate a string signature of a function. |
|
|
|
:param width: Fold lines if a line is longer than this value. |
|
:type width: int |
|
:arg func_name: Override function name when given. |
|
:type func_name: str |
|
|
|
:rtype: str |
|
""" |
|
|
|
if call_string is None: |
|
if funcdef.type == 'lambdef': |
|
call_string = '<lambda>' |
|
else: |
|
call_string = funcdef.name.value |
|
params = funcdef.get_params() |
|
if omit_first_param: |
|
params = params[1:] |
|
p = '(' + ''.join(param.get_code() for param in params).strip() + ')' |
|
|
|
p = re.sub(r'\s+', ' ', p) |
|
if funcdef.annotation and not omit_return_annotation: |
|
rtype = " ->" + funcdef.annotation.get_code() |
|
else: |
|
rtype = "" |
|
code = call_string + p + rtype |
|
|
|
return '\n'.join(textwrap.wrap(code, width)) |
|
|
|
|
|
def move(node, line_offset): |
|
""" |
|
Move the `Node` start_pos. |
|
""" |
|
try: |
|
children = node.children |
|
except AttributeError: |
|
node.line += line_offset |
|
else: |
|
for c in children: |
|
move(c, line_offset) |
|
|
|
|
|
def get_following_comment_same_line(node): |
|
""" |
|
returns (as string) any comment that appears on the same line, |
|
after the node, including the # |
|
""" |
|
try: |
|
if node.type == 'for_stmt': |
|
whitespace = node.children[5].get_first_leaf().prefix |
|
elif node.type == 'with_stmt': |
|
whitespace = node.children[3].get_first_leaf().prefix |
|
elif node.type == 'funcdef': |
|
|
|
whitespace = node.children[4].get_first_leaf().get_next_leaf().prefix |
|
else: |
|
whitespace = node.get_last_leaf().get_next_leaf().prefix |
|
except AttributeError: |
|
return None |
|
except ValueError: |
|
|
|
|
|
return None |
|
if "#" not in whitespace: |
|
return None |
|
comment = whitespace[whitespace.index("#"):] |
|
if "\r" in comment: |
|
comment = comment[:comment.index("\r")] |
|
if "\n" in comment: |
|
comment = comment[:comment.index("\n")] |
|
return comment |
|
|
|
|
|
def is_scope(node): |
|
t = node.type |
|
if t == 'comp_for': |
|
|
|
return node.children[1].type != 'sync_comp_for' |
|
|
|
return t in ('file_input', 'classdef', 'funcdef', 'lambdef', 'sync_comp_for') |
|
|
|
|
|
def _get_parent_scope_cache(func): |
|
cache = WeakKeyDictionary() |
|
|
|
def wrapper(parso_cache_node, node, include_flows=False): |
|
if parso_cache_node is None: |
|
return func(node, include_flows) |
|
|
|
try: |
|
for_module = cache[parso_cache_node] |
|
except KeyError: |
|
for_module = cache[parso_cache_node] = {} |
|
|
|
try: |
|
return for_module[node] |
|
except KeyError: |
|
result = for_module[node] = func(node, include_flows) |
|
return result |
|
return wrapper |
|
|
|
|
|
def get_parent_scope(node, include_flows=False): |
|
""" |
|
Returns the underlying scope. |
|
""" |
|
scope = node.parent |
|
if scope is None: |
|
return None |
|
|
|
while True: |
|
if is_scope(scope): |
|
if scope.type in ('classdef', 'funcdef', 'lambdef'): |
|
index = scope.children.index(':') |
|
if scope.children[index].start_pos >= node.start_pos: |
|
if node.parent.type == 'param' and node.parent.name == node: |
|
pass |
|
elif node.parent.type == 'tfpdef' and node.parent.children[0] == node: |
|
pass |
|
else: |
|
scope = scope.parent |
|
continue |
|
return scope |
|
elif include_flows and isinstance(scope, tree.Flow): |
|
|
|
|
|
if not (scope.type == 'if_stmt' |
|
and any(n.start_pos <= node.start_pos < n.end_pos |
|
for n in scope.get_test_nodes())): |
|
return scope |
|
|
|
scope = scope.parent |
|
|
|
|
|
get_cached_parent_scope = _get_parent_scope_cache(get_parent_scope) |
|
|
|
|
|
def get_cached_code_lines(grammar, path): |
|
""" |
|
Basically access the cached code lines in parso. This is not the nicest way |
|
to do this, but we avoid splitting all the lines again. |
|
""" |
|
return get_parso_cache_node(grammar, path).lines |
|
|
|
|
|
def get_parso_cache_node(grammar, path): |
|
""" |
|
This is of course not public. But as long as I control parso, this |
|
shouldn't be a problem. ~ Dave |
|
|
|
The reason for this is mostly caching. This is obviously also a sign of a |
|
broken caching architecture. |
|
""" |
|
return parser_cache[grammar._hashed][path] |
|
|
|
|
|
def cut_value_at_position(leaf, position): |
|
""" |
|
Cuts of the value of the leaf at position |
|
""" |
|
lines = split_lines(leaf.value, keepends=True)[:position[0] - leaf.line + 1] |
|
column = position[1] |
|
if leaf.line == position[0]: |
|
column -= leaf.column |
|
if not lines: |
|
return '' |
|
lines[-1] = lines[-1][:column] |
|
return ''.join(lines) |
|
|
|
|
|
def expr_is_dotted(node): |
|
""" |
|
Checks if a path looks like `name` or `name.foo.bar` and not `name()`. |
|
""" |
|
if node.type == 'atom': |
|
if len(node.children) == 3 and node.children[0] == '(': |
|
return expr_is_dotted(node.children[1]) |
|
return False |
|
if node.type == 'atom_expr': |
|
children = node.children |
|
if children[0] == 'await': |
|
return False |
|
if not expr_is_dotted(children[0]): |
|
return False |
|
|
|
return all(c.children[0] == '.' for c in children[1:]) |
|
return node.type == 'name' |
|
|
|
|
|
def _function_is_x_method(*method_names): |
|
def wrapper(function_node): |
|
""" |
|
This is a heuristic. It will not hold ALL the times, but it will be |
|
correct pretty much for anyone that doesn't try to beat it. |
|
staticmethod/classmethod are builtins and unless overwritten, this will |
|
be correct. |
|
""" |
|
for decorator in function_node.get_decorators(): |
|
dotted_name = decorator.children[1] |
|
if dotted_name.get_code() in method_names: |
|
return True |
|
return False |
|
return wrapper |
|
|
|
|
|
function_is_staticmethod = _function_is_x_method('staticmethod') |
|
function_is_classmethod = _function_is_x_method('classmethod') |
|
function_is_property = _function_is_x_method('property', 'cached_property') |
|
|