155 lines
5.0 KiB
Python
155 lines
5.0 KiB
Python
|
# coding=utf-8
|
||
|
"""Rename names in a python module."""
|
||
|
# Copyright 2017 Google LLC
|
||
|
#
|
||
|
# 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
|
||
|
#
|
||
|
# https://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.
|
||
|
|
||
|
from __future__ import absolute_import
|
||
|
from __future__ import division
|
||
|
from __future__ import print_function
|
||
|
|
||
|
import ast
|
||
|
import six
|
||
|
|
||
|
from pasta.augment import import_utils
|
||
|
from pasta.base import ast_utils
|
||
|
from pasta.base import scope
|
||
|
|
||
|
|
||
|
def rename_external(t, old_name, new_name):
|
||
|
"""Rename an imported name in a module.
|
||
|
|
||
|
This will rewrite all import statements in `tree` that reference the old
|
||
|
module as well as any names in `tree` which reference the imported name. This
|
||
|
may introduce new import statements, but only if necessary.
|
||
|
|
||
|
For example, to move and rename the module `foo.bar.utils` to `foo.bar_utils`:
|
||
|
> rename_external(tree, 'foo.bar.utils', 'foo.bar_utils')
|
||
|
|
||
|
- import foo.bar.utils
|
||
|
+ import foo.bar_utils
|
||
|
|
||
|
- from foo.bar import utils
|
||
|
+ from foo import bar_utils
|
||
|
|
||
|
- from foo.bar import logic, utils
|
||
|
+ from foo.bar import logic
|
||
|
+ from foo import bar_utils
|
||
|
|
||
|
Arguments:
|
||
|
t: (ast.Module) Module syntax tree to perform the rename in. This will be
|
||
|
updated as a result of this function call with all affected nodes changed
|
||
|
and potentially new Import/ImportFrom nodes added.
|
||
|
old_name: (string) Fully-qualified path of the name to replace.
|
||
|
new_name: (string) Fully-qualified path of the name to update to.
|
||
|
|
||
|
Returns:
|
||
|
True if any changes were made, False otherwise.
|
||
|
"""
|
||
|
sc = scope.analyze(t)
|
||
|
|
||
|
if old_name not in sc.external_references:
|
||
|
return False
|
||
|
|
||
|
has_changed = False
|
||
|
renames = {}
|
||
|
already_changed = []
|
||
|
for ref in sc.external_references[old_name]:
|
||
|
if isinstance(ref.node, ast.alias):
|
||
|
parent = sc.parent(ref.node)
|
||
|
# An alias may be the most specific reference to an imported name, but it
|
||
|
# could if it is a child of an ImportFrom, the ImportFrom node's module
|
||
|
# may also need to be updated.
|
||
|
if isinstance(parent, ast.ImportFrom) and parent not in already_changed:
|
||
|
assert _rename_name_in_importfrom(sc, parent, old_name, new_name)
|
||
|
renames[old_name.rsplit('.', 1)[-1]] = new_name.rsplit('.', 1)[-1]
|
||
|
already_changed.append(parent)
|
||
|
else:
|
||
|
ref.node.name = new_name + ref.node.name[len(old_name):]
|
||
|
if not ref.node.asname:
|
||
|
renames[old_name] = new_name
|
||
|
has_changed = True
|
||
|
elif isinstance(ref.node, ast.ImportFrom):
|
||
|
if ref.node not in already_changed:
|
||
|
assert _rename_name_in_importfrom(sc, ref.node, old_name, new_name)
|
||
|
renames[old_name.rsplit('.', 1)[-1]] = new_name.rsplit('.', 1)[-1]
|
||
|
already_changed.append(ref.node)
|
||
|
has_changed = True
|
||
|
|
||
|
for rename_old, rename_new in six.iteritems(renames):
|
||
|
_rename_reads(sc, t, rename_old, rename_new)
|
||
|
return has_changed
|
||
|
|
||
|
|
||
|
def _rename_name_in_importfrom(sc, node, old_name, new_name):
|
||
|
if old_name == new_name:
|
||
|
return False
|
||
|
|
||
|
module_parts = node.module.split('.')
|
||
|
old_parts = old_name.split('.')
|
||
|
new_parts = new_name.split('.')
|
||
|
|
||
|
# If just the module is changing, rename it
|
||
|
if module_parts[:len(old_parts)] == old_parts:
|
||
|
node.module = '.'.join(new_parts + module_parts[len(old_parts):])
|
||
|
return True
|
||
|
|
||
|
# Find the alias node to be changed
|
||
|
for alias_to_change in node.names:
|
||
|
if alias_to_change.name == old_parts[-1]:
|
||
|
break
|
||
|
else:
|
||
|
return False
|
||
|
|
||
|
alias_to_change.name = new_parts[-1]
|
||
|
|
||
|
# Split the import if the package has changed
|
||
|
if module_parts != new_parts[:-1]:
|
||
|
if len(node.names) > 1:
|
||
|
new_import = import_utils.split_import(sc, node, alias_to_change)
|
||
|
new_import.module = '.'.join(new_parts[:-1])
|
||
|
else:
|
||
|
node.module = '.'.join(new_parts[:-1])
|
||
|
|
||
|
return True
|
||
|
|
||
|
|
||
|
def _rename_reads(sc, t, old_name, new_name):
|
||
|
"""Updates all locations in the module where the given name is read.
|
||
|
|
||
|
Arguments:
|
||
|
sc: (scope.Scope) Scope to work in. This should be the scope of `t`.
|
||
|
t: (ast.AST) The AST to perform updates in.
|
||
|
old_name: (string) Dotted name to update.
|
||
|
new_name: (string) Dotted name to replace it with.
|
||
|
|
||
|
Returns:
|
||
|
True if any changes were made, False otherwise.
|
||
|
"""
|
||
|
name_parts = old_name.split('.')
|
||
|
try:
|
||
|
name = sc.names[name_parts[0]]
|
||
|
for part in name_parts[1:]:
|
||
|
name = name.attrs[part]
|
||
|
except KeyError:
|
||
|
return False
|
||
|
|
||
|
has_changed = False
|
||
|
for ref_node in name.reads:
|
||
|
if isinstance(ref_node, (ast.Name, ast.Attribute)):
|
||
|
ast_utils.replace_child(sc.parent(ref_node), ref_node,
|
||
|
ast.parse(new_name).body[0].value)
|
||
|
has_changed = True
|
||
|
|
||
|
return has_changed
|