2020-06-10 16:04:54 +00:00
|
|
|
"""
|
|
|
|
Utility functions from 2to3, 3to2 and python-modernize (and some home-grown
|
|
|
|
ones).
|
|
|
|
|
|
|
|
Licences:
|
|
|
|
2to3: PSF License v2
|
|
|
|
3to2: Apache Software License (from 3to2/setup.py)
|
|
|
|
python-modernize licence: BSD (from python-modernize/LICENSE)
|
|
|
|
"""
|
|
|
|
|
|
|
|
from lib2to3.fixer_util import (FromImport, Newline, is_import,
|
2024-03-03 17:15:23 +00:00
|
|
|
find_root, does_tree_import,
|
|
|
|
Call, Name, Comma)
|
2020-06-10 16:04:54 +00:00
|
|
|
from lib2to3.pytree import Leaf, Node
|
2024-03-03 17:15:23 +00:00
|
|
|
from lib2to3.pygram import python_symbols as syms
|
2020-06-10 16:04:54 +00:00
|
|
|
from lib2to3.pygram import token
|
|
|
|
import re
|
|
|
|
|
|
|
|
|
|
|
|
def canonical_fix_name(fix, avail_fixes):
|
|
|
|
"""
|
|
|
|
Examples:
|
|
|
|
>>> canonical_fix_name('fix_wrap_text_literals')
|
|
|
|
'libfuturize.fixes.fix_wrap_text_literals'
|
|
|
|
>>> canonical_fix_name('wrap_text_literals')
|
|
|
|
'libfuturize.fixes.fix_wrap_text_literals'
|
|
|
|
>>> canonical_fix_name('wrap_te')
|
|
|
|
ValueError("unknown fixer name")
|
|
|
|
>>> canonical_fix_name('wrap')
|
|
|
|
ValueError("ambiguous fixer name")
|
|
|
|
"""
|
|
|
|
if ".fix_" in fix:
|
|
|
|
return fix
|
|
|
|
else:
|
|
|
|
if fix.startswith('fix_'):
|
|
|
|
fix = fix[4:]
|
|
|
|
# Infer the full module name for the fixer.
|
|
|
|
# First ensure that no names clash (e.g.
|
|
|
|
# lib2to3.fixes.fix_blah and libfuturize.fixes.fix_blah):
|
|
|
|
found = [f for f in avail_fixes
|
|
|
|
if f.endswith('fix_{0}'.format(fix))]
|
|
|
|
if len(found) > 1:
|
|
|
|
raise ValueError("Ambiguous fixer name. Choose a fully qualified "
|
|
|
|
"module name instead from these:\n" +
|
|
|
|
"\n".join(" " + myf for myf in found))
|
|
|
|
elif len(found) == 0:
|
|
|
|
raise ValueError("Unknown fixer. Use --list-fixes or -l for a list.")
|
|
|
|
return found[0]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## These functions are from 3to2 by Joe Amenta:
|
|
|
|
|
|
|
|
def Star(prefix=None):
|
|
|
|
return Leaf(token.STAR, u'*', prefix=prefix)
|
|
|
|
|
|
|
|
def DoubleStar(prefix=None):
|
|
|
|
return Leaf(token.DOUBLESTAR, u'**', prefix=prefix)
|
|
|
|
|
|
|
|
def Minus(prefix=None):
|
|
|
|
return Leaf(token.MINUS, u'-', prefix=prefix)
|
|
|
|
|
|
|
|
def commatize(leafs):
|
|
|
|
"""
|
|
|
|
Accepts/turns: (Name, Name, ..., Name, Name)
|
|
|
|
Returns/into: (Name, Comma, Name, Comma, ..., Name, Comma, Name)
|
|
|
|
"""
|
|
|
|
new_leafs = []
|
|
|
|
for leaf in leafs:
|
|
|
|
new_leafs.append(leaf)
|
|
|
|
new_leafs.append(Comma())
|
|
|
|
del new_leafs[-1]
|
|
|
|
return new_leafs
|
|
|
|
|
|
|
|
def indentation(node):
|
|
|
|
"""
|
|
|
|
Returns the indentation for this node
|
|
|
|
Iff a node is in a suite, then it has indentation.
|
|
|
|
"""
|
|
|
|
while node.parent is not None and node.parent.type != syms.suite:
|
|
|
|
node = node.parent
|
|
|
|
if node.parent is None:
|
|
|
|
return u""
|
|
|
|
# The first three children of a suite are NEWLINE, INDENT, (some other node)
|
|
|
|
# INDENT.value contains the indentation for this suite
|
|
|
|
# anything after (some other node) has the indentation as its prefix.
|
|
|
|
if node.type == token.INDENT:
|
|
|
|
return node.value
|
|
|
|
elif node.prev_sibling is not None and node.prev_sibling.type == token.INDENT:
|
|
|
|
return node.prev_sibling.value
|
|
|
|
elif node.prev_sibling is None:
|
|
|
|
return u""
|
|
|
|
else:
|
|
|
|
return node.prefix
|
|
|
|
|
|
|
|
def indentation_step(node):
|
|
|
|
"""
|
|
|
|
Dirty little trick to get the difference between each indentation level
|
|
|
|
Implemented by finding the shortest indentation string
|
|
|
|
(technically, the "least" of all of the indentation strings, but
|
|
|
|
tabs and spaces mixed won't get this far, so those are synonymous.)
|
|
|
|
"""
|
|
|
|
r = find_root(node)
|
|
|
|
# Collect all indentations into one set.
|
|
|
|
all_indents = set(i.value for i in r.pre_order() if i.type == token.INDENT)
|
|
|
|
if not all_indents:
|
|
|
|
# nothing is indented anywhere, so we get to pick what we want
|
|
|
|
return u" " # four spaces is a popular convention
|
|
|
|
else:
|
|
|
|
return min(all_indents)
|
|
|
|
|
|
|
|
def suitify(parent):
|
|
|
|
"""
|
|
|
|
Turn the stuff after the first colon in parent's children
|
|
|
|
into a suite, if it wasn't already
|
|
|
|
"""
|
|
|
|
for node in parent.children:
|
|
|
|
if node.type == syms.suite:
|
2024-03-03 17:15:23 +00:00
|
|
|
# already in the preferred format, do nothing
|
2020-06-10 16:04:54 +00:00
|
|
|
return
|
|
|
|
|
|
|
|
# One-liners have no suite node, we have to fake one up
|
|
|
|
for i, node in enumerate(parent.children):
|
|
|
|
if node.type == token.COLON:
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
raise ValueError(u"No class suite and no ':'!")
|
|
|
|
# Move everything into a suite node
|
|
|
|
suite = Node(syms.suite, [Newline(), Leaf(token.INDENT, indentation(node) + indentation_step(node))])
|
|
|
|
one_node = parent.children[i+1]
|
|
|
|
one_node.remove()
|
|
|
|
one_node.prefix = u''
|
|
|
|
suite.append_child(one_node)
|
|
|
|
parent.append_child(suite)
|
|
|
|
|
|
|
|
def NameImport(package, as_name=None, prefix=None):
|
|
|
|
"""
|
|
|
|
Accepts a package (Name node), name to import it as (string), and
|
|
|
|
optional prefix and returns a node:
|
|
|
|
import <package> [as <as_name>]
|
|
|
|
"""
|
|
|
|
if prefix is None:
|
|
|
|
prefix = u""
|
|
|
|
children = [Name(u"import", prefix=prefix), package]
|
|
|
|
if as_name is not None:
|
|
|
|
children.extend([Name(u"as", prefix=u" "),
|
|
|
|
Name(as_name, prefix=u" ")])
|
|
|
|
return Node(syms.import_name, children)
|
|
|
|
|
|
|
|
_compound_stmts = (syms.if_stmt, syms.while_stmt, syms.for_stmt, syms.try_stmt, syms.with_stmt)
|
|
|
|
_import_stmts = (syms.import_name, syms.import_from)
|
|
|
|
|
|
|
|
def import_binding_scope(node):
|
|
|
|
"""
|
|
|
|
Generator yields all nodes for which a node (an import_stmt) has scope
|
|
|
|
The purpose of this is for a call to _find() on each of them
|
|
|
|
"""
|
|
|
|
# import_name / import_from are small_stmts
|
|
|
|
assert node.type in _import_stmts
|
|
|
|
test = node.next_sibling
|
|
|
|
# A small_stmt can only be followed by a SEMI or a NEWLINE.
|
|
|
|
while test.type == token.SEMI:
|
|
|
|
nxt = test.next_sibling
|
|
|
|
# A SEMI can only be followed by a small_stmt or a NEWLINE
|
|
|
|
if nxt.type == token.NEWLINE:
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
yield nxt
|
|
|
|
# A small_stmt can only be followed by either a SEMI or a NEWLINE
|
|
|
|
test = nxt.next_sibling
|
|
|
|
# Covered all subsequent small_stmts after the import_stmt
|
|
|
|
# Now to cover all subsequent stmts after the parent simple_stmt
|
|
|
|
parent = node.parent
|
|
|
|
assert parent.type == syms.simple_stmt
|
|
|
|
test = parent.next_sibling
|
|
|
|
while test is not None:
|
|
|
|
# Yes, this will yield NEWLINE and DEDENT. Deal with it.
|
|
|
|
yield test
|
|
|
|
test = test.next_sibling
|
|
|
|
|
|
|
|
context = parent.parent
|
|
|
|
# Recursively yield nodes following imports inside of a if/while/for/try/with statement
|
|
|
|
if context.type in _compound_stmts:
|
|
|
|
# import is in a one-liner
|
|
|
|
c = context
|
|
|
|
while c.next_sibling is not None:
|
|
|
|
yield c.next_sibling
|
|
|
|
c = c.next_sibling
|
|
|
|
context = context.parent
|
|
|
|
|
|
|
|
# Can't chain one-liners on one line, so that takes care of that.
|
|
|
|
|
|
|
|
p = context.parent
|
|
|
|
if p is None:
|
|
|
|
return
|
|
|
|
|
|
|
|
# in a multi-line suite
|
|
|
|
|
|
|
|
while p.type in _compound_stmts:
|
|
|
|
|
|
|
|
if context.type == syms.suite:
|
|
|
|
yield context
|
|
|
|
|
|
|
|
context = context.next_sibling
|
|
|
|
|
|
|
|
if context is None:
|
|
|
|
context = p.parent
|
|
|
|
p = context.parent
|
|
|
|
if p is None:
|
|
|
|
break
|
|
|
|
|
|
|
|
def ImportAsName(name, as_name, prefix=None):
|
|
|
|
new_name = Name(name)
|
|
|
|
new_as = Name(u"as", prefix=u" ")
|
|
|
|
new_as_name = Name(as_name, prefix=u" ")
|
|
|
|
new_node = Node(syms.import_as_name, [new_name, new_as, new_as_name])
|
|
|
|
if prefix is not None:
|
|
|
|
new_node.prefix = prefix
|
|
|
|
return new_node
|
|
|
|
|
|
|
|
|
|
|
|
def is_docstring(node):
|
|
|
|
"""
|
|
|
|
Returns True if the node appears to be a docstring
|
|
|
|
"""
|
|
|
|
return (node.type == syms.simple_stmt and
|
|
|
|
len(node.children) > 0 and node.children[0].type == token.STRING)
|
|
|
|
|
|
|
|
|
|
|
|
def future_import(feature, node):
|
|
|
|
"""
|
|
|
|
This seems to work
|
|
|
|
"""
|
|
|
|
root = find_root(node)
|
|
|
|
|
|
|
|
if does_tree_import(u"__future__", feature, node):
|
|
|
|
return
|
|
|
|
|
|
|
|
# Look for a shebang or encoding line
|
|
|
|
shebang_encoding_idx = None
|
|
|
|
|
|
|
|
for idx, node in enumerate(root.children):
|
|
|
|
# Is it a shebang or encoding line?
|
|
|
|
if is_shebang_comment(node) or is_encoding_comment(node):
|
|
|
|
shebang_encoding_idx = idx
|
|
|
|
if is_docstring(node):
|
|
|
|
# skip over docstring
|
|
|
|
continue
|
|
|
|
names = check_future_import(node)
|
|
|
|
if not names:
|
|
|
|
# not a future statement; need to insert before this
|
|
|
|
break
|
|
|
|
if feature in names:
|
|
|
|
# already imported
|
|
|
|
return
|
|
|
|
|
|
|
|
import_ = FromImport(u'__future__', [Leaf(token.NAME, feature, prefix=" ")])
|
|
|
|
if shebang_encoding_idx == 0 and idx == 0:
|
|
|
|
# If this __future__ import would go on the first line,
|
|
|
|
# detach the shebang / encoding prefix from the current first line.
|
|
|
|
# and attach it to our new __future__ import node.
|
|
|
|
import_.prefix = root.children[0].prefix
|
|
|
|
root.children[0].prefix = u''
|
|
|
|
# End the __future__ import line with a newline and add a blank line
|
|
|
|
# afterwards:
|
|
|
|
children = [import_ , Newline()]
|
|
|
|
root.insert_child(idx, Node(syms.simple_stmt, children))
|
|
|
|
|
|
|
|
|
|
|
|
def future_import2(feature, node):
|
|
|
|
"""
|
|
|
|
An alternative to future_import() which might not work ...
|
|
|
|
"""
|
|
|
|
root = find_root(node)
|
|
|
|
|
|
|
|
if does_tree_import(u"__future__", feature, node):
|
|
|
|
return
|
|
|
|
|
|
|
|
insert_pos = 0
|
|
|
|
for idx, node in enumerate(root.children):
|
|
|
|
if node.type == syms.simple_stmt and node.children and \
|
|
|
|
node.children[0].type == token.STRING:
|
|
|
|
insert_pos = idx + 1
|
|
|
|
break
|
|
|
|
|
|
|
|
for thing_after in root.children[insert_pos:]:
|
|
|
|
if thing_after.type == token.NEWLINE:
|
|
|
|
insert_pos += 1
|
|
|
|
continue
|
|
|
|
|
|
|
|
prefix = thing_after.prefix
|
|
|
|
thing_after.prefix = u""
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
prefix = u""
|
|
|
|
|
|
|
|
import_ = FromImport(u"__future__", [Leaf(token.NAME, feature, prefix=u" ")])
|
|
|
|
|
|
|
|
children = [import_, Newline()]
|
|
|
|
root.insert_child(insert_pos, Node(syms.simple_stmt, children, prefix=prefix))
|
|
|
|
|
|
|
|
def parse_args(arglist, scheme):
|
|
|
|
u"""
|
|
|
|
Parse a list of arguments into a dict
|
|
|
|
"""
|
|
|
|
arglist = [i for i in arglist if i.type != token.COMMA]
|
|
|
|
|
|
|
|
ret_mapping = dict([(k, None) for k in scheme])
|
|
|
|
|
|
|
|
for i, arg in enumerate(arglist):
|
|
|
|
if arg.type == syms.argument and arg.children[1].type == token.EQUAL:
|
|
|
|
# argument < NAME '=' any >
|
|
|
|
slot = arg.children[0].value
|
|
|
|
ret_mapping[slot] = arg.children[2]
|
|
|
|
else:
|
|
|
|
slot = scheme[i]
|
|
|
|
ret_mapping[slot] = arg
|
|
|
|
|
|
|
|
return ret_mapping
|
|
|
|
|
|
|
|
|
|
|
|
# def is_import_from(node):
|
|
|
|
# """Returns true if the node is a statement "from ... import ..."
|
|
|
|
# """
|
|
|
|
# return node.type == syms.import_from
|
|
|
|
|
|
|
|
|
|
|
|
def is_import_stmt(node):
|
|
|
|
return (node.type == syms.simple_stmt and node.children and
|
|
|
|
is_import(node.children[0]))
|
|
|
|
|
|
|
|
|
|
|
|
def touch_import_top(package, name_to_import, node):
|
|
|
|
"""Works like `does_tree_import` but adds an import statement at the
|
|
|
|
top if it was not imported (but below any __future__ imports) and below any
|
|
|
|
comments such as shebang lines).
|
|
|
|
|
|
|
|
Based on lib2to3.fixer_util.touch_import()
|
|
|
|
|
|
|
|
Calling this multiple times adds the imports in reverse order.
|
|
|
|
|
|
|
|
Also adds "standard_library.install_aliases()" after "from future import
|
|
|
|
standard_library". This should probably be factored into another function.
|
|
|
|
"""
|
|
|
|
|
|
|
|
root = find_root(node)
|
|
|
|
|
|
|
|
if does_tree_import(package, name_to_import, root):
|
|
|
|
return
|
|
|
|
|
|
|
|
# Ideally, we would look for whether futurize --all-imports has been run,
|
|
|
|
# as indicated by the presence of ``from builtins import (ascii, ...,
|
|
|
|
# zip)`` -- and, if it has, we wouldn't import the name again.
|
|
|
|
|
|
|
|
# Look for __future__ imports and insert below them
|
|
|
|
found = False
|
|
|
|
for name in ['absolute_import', 'division', 'print_function',
|
|
|
|
'unicode_literals']:
|
|
|
|
if does_tree_import('__future__', name, root):
|
|
|
|
found = True
|
|
|
|
break
|
|
|
|
if found:
|
|
|
|
# At least one __future__ import. We want to loop until we've seen them
|
|
|
|
# all.
|
|
|
|
start, end = None, None
|
|
|
|
for idx, node in enumerate(root.children):
|
|
|
|
if check_future_import(node):
|
|
|
|
start = idx
|
|
|
|
# Start looping
|
|
|
|
idx2 = start
|
|
|
|
while node:
|
|
|
|
node = node.next_sibling
|
|
|
|
idx2 += 1
|
|
|
|
if not check_future_import(node):
|
|
|
|
end = idx2
|
|
|
|
break
|
|
|
|
break
|
|
|
|
assert start is not None
|
|
|
|
assert end is not None
|
|
|
|
insert_pos = end
|
|
|
|
else:
|
|
|
|
# No __future__ imports.
|
|
|
|
# We look for a docstring and insert the new node below that. If no docstring
|
|
|
|
# exists, just insert the node at the top.
|
|
|
|
for idx, node in enumerate(root.children):
|
|
|
|
if node.type != syms.simple_stmt:
|
|
|
|
break
|
|
|
|
if not is_docstring(node):
|
|
|
|
# This is the usual case.
|
|
|
|
break
|
|
|
|
insert_pos = idx
|
|
|
|
|
2024-03-03 17:15:23 +00:00
|
|
|
children_hooks = []
|
2020-06-10 16:04:54 +00:00
|
|
|
if package is None:
|
|
|
|
import_ = Node(syms.import_name, [
|
|
|
|
Leaf(token.NAME, u"import"),
|
|
|
|
Leaf(token.NAME, name_to_import, prefix=u" ")
|
|
|
|
])
|
|
|
|
else:
|
|
|
|
import_ = FromImport(package, [Leaf(token.NAME, name_to_import, prefix=u" ")])
|
|
|
|
if name_to_import == u'standard_library':
|
|
|
|
# Add:
|
|
|
|
# standard_library.install_aliases()
|
|
|
|
# after:
|
|
|
|
# from future import standard_library
|
|
|
|
install_hooks = Node(syms.simple_stmt,
|
|
|
|
[Node(syms.power,
|
|
|
|
[Leaf(token.NAME, u'standard_library'),
|
|
|
|
Node(syms.trailer, [Leaf(token.DOT, u'.'),
|
|
|
|
Leaf(token.NAME, u'install_aliases')]),
|
|
|
|
Node(syms.trailer, [Leaf(token.LPAR, u'('),
|
|
|
|
Leaf(token.RPAR, u')')])
|
|
|
|
])
|
|
|
|
]
|
|
|
|
)
|
|
|
|
children_hooks = [install_hooks, Newline()]
|
|
|
|
|
|
|
|
# FromImport(package, [Leaf(token.NAME, name_to_import, prefix=u" ")])
|
|
|
|
|
|
|
|
children_import = [import_, Newline()]
|
|
|
|
old_prefix = root.children[insert_pos].prefix
|
|
|
|
root.children[insert_pos].prefix = u''
|
|
|
|
root.insert_child(insert_pos, Node(syms.simple_stmt, children_import, prefix=old_prefix))
|
|
|
|
if len(children_hooks) > 0:
|
|
|
|
root.insert_child(insert_pos + 1, Node(syms.simple_stmt, children_hooks))
|
|
|
|
|
|
|
|
|
|
|
|
## The following functions are from python-modernize by Armin Ronacher:
|
|
|
|
# (a little edited).
|
|
|
|
|
|
|
|
def check_future_import(node):
|
|
|
|
"""If this is a future import, return set of symbols that are imported,
|
|
|
|
else return None."""
|
|
|
|
# node should be the import statement here
|
|
|
|
savenode = node
|
|
|
|
if not (node.type == syms.simple_stmt and node.children):
|
|
|
|
return set()
|
|
|
|
node = node.children[0]
|
|
|
|
# now node is the import_from node
|
|
|
|
if not (node.type == syms.import_from and
|
|
|
|
# node.type == token.NAME and # seems to break it
|
|
|
|
hasattr(node.children[1], 'value') and
|
|
|
|
node.children[1].value == u'__future__'):
|
|
|
|
return set()
|
|
|
|
if node.children[3].type == token.LPAR:
|
|
|
|
node = node.children[4]
|
|
|
|
else:
|
|
|
|
node = node.children[3]
|
|
|
|
# now node is the import_as_name[s]
|
|
|
|
if node.type == syms.import_as_names:
|
|
|
|
result = set()
|
|
|
|
for n in node.children:
|
|
|
|
if n.type == token.NAME:
|
|
|
|
result.add(n.value)
|
|
|
|
elif n.type == syms.import_as_name:
|
|
|
|
n = n.children[0]
|
|
|
|
assert n.type == token.NAME
|
|
|
|
result.add(n.value)
|
|
|
|
return result
|
|
|
|
elif node.type == syms.import_as_name:
|
|
|
|
node = node.children[0]
|
|
|
|
assert node.type == token.NAME
|
|
|
|
return set([node.value])
|
|
|
|
elif node.type == token.NAME:
|
|
|
|
return set([node.value])
|
|
|
|
else:
|
|
|
|
# TODO: handle brackets like this:
|
|
|
|
# from __future__ import (absolute_import, division)
|
|
|
|
assert False, "strange import: %s" % savenode
|
|
|
|
|
|
|
|
|
|
|
|
SHEBANG_REGEX = r'^#!.*python'
|
|
|
|
ENCODING_REGEX = r"^#.*coding[:=]\s*([-\w.]+)"
|
|
|
|
|
|
|
|
|
|
|
|
def is_shebang_comment(node):
|
|
|
|
"""
|
|
|
|
Comments are prefixes for Leaf nodes. Returns whether the given node has a
|
|
|
|
prefix that looks like a shebang line or an encoding line:
|
|
|
|
|
|
|
|
#!/usr/bin/env python
|
|
|
|
#!/usr/bin/python3
|
|
|
|
"""
|
|
|
|
return bool(re.match(SHEBANG_REGEX, node.prefix))
|
|
|
|
|
|
|
|
|
|
|
|
def is_encoding_comment(node):
|
|
|
|
"""
|
|
|
|
Comments are prefixes for Leaf nodes. Returns whether the given node has a
|
|
|
|
prefix that looks like an encoding line:
|
|
|
|
|
|
|
|
# coding: utf-8
|
|
|
|
# encoding: utf-8
|
|
|
|
# -*- coding: <encoding name> -*-
|
|
|
|
# vim: set fileencoding=<encoding name> :
|
|
|
|
"""
|
|
|
|
return bool(re.match(ENCODING_REGEX, node.prefix))
|
|
|
|
|
|
|
|
|
|
|
|
def wrap_in_fn_call(fn_name, args, prefix=None):
|
|
|
|
"""
|
|
|
|
Example:
|
|
|
|
>>> wrap_in_fn_call("oldstr", (arg,))
|
|
|
|
oldstr(arg)
|
|
|
|
|
|
|
|
>>> wrap_in_fn_call("olddiv", (arg1, arg2))
|
|
|
|
olddiv(arg1, arg2)
|
|
|
|
|
|
|
|
>>> wrap_in_fn_call("olddiv", [arg1, comma, arg2, comma, arg3])
|
|
|
|
olddiv(arg1, arg2, arg3)
|
|
|
|
"""
|
|
|
|
assert len(args) > 0
|
|
|
|
if len(args) == 2:
|
|
|
|
expr1, expr2 = args
|
|
|
|
newargs = [expr1, Comma(), expr2]
|
|
|
|
else:
|
|
|
|
newargs = args
|
|
|
|
return Call(Name(fn_name), newargs, prefix=prefix)
|