1# Copyright 2015 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5from __future__ import print_function
6
7import functools
8import os
9import sys
10
11from py_utils import refactor
12
13
14def Run(sources, target, files_to_update):
15  """Move modules and update imports.
16
17  Args:
18    sources: List of source module or package paths.
19    target: Destination module or package path.
20    files_to_update: Modules whose imports we should check for changes.
21  """
22  # TODO(dtu): Support moving classes and functions.
23  moves = tuple(_Move(source, target) for source in sources)
24
25  # Update imports and references.
26  refactor.Transform(functools.partial(_Update, moves), files_to_update)
27
28  # Move files.
29  for move in moves:
30    os.rename(move.source_path, move.target_path)
31
32
33def _Update(moves, module):
34  for import_statement in module.FindAll(refactor.Import):
35    for move in moves:
36      try:
37        if move.UpdateImportAndReferences(module, import_statement):
38          break
39      except NotImplementedError as e:
40        print('Error updating %s: %s' % (module.file_path, e), file=sys.stderr)
41
42
43class _Move(object):
44
45  def __init__(self, source, target):
46    self._source_path = os.path.realpath(source)
47    self._target_path = os.path.realpath(target)
48
49    if os.path.isdir(self._target_path):
50      self._target_path = os.path.join(
51          self._target_path, os.path.basename(self._source_path))
52
53  @property
54  def source_path(self):
55    return self._source_path
56
57  @property
58  def target_path(self):
59    return self._target_path
60
61  @property
62  def source_module_path(self):
63    return _ModulePath(self._source_path)
64
65  @property
66  def target_module_path(self):
67    return _ModulePath(self._target_path)
68
69  def UpdateImportAndReferences(self, module, import_statement):
70    """Update an import statement in a module and all its references..
71
72    Args:
73      module: The refactor.Module to update.
74      import_statement:  The refactor.Import to update.
75
76    Returns:
77      True if the import statement was updated, or False if the import statement
78      needed no updating.
79    """
80    statement_path_parts = import_statement.path.split('.')
81    source_path_parts = self.source_module_path.split('.')
82    if source_path_parts != statement_path_parts[:len(source_path_parts)]:
83      return False
84
85    # Update import statement.
86    old_name_parts = import_statement.name.split('.')
87    new_name_parts = ([self.target_module_path] +
88                      statement_path_parts[len(source_path_parts):])
89    import_statement.path = '.'.join(new_name_parts)
90    new_name = import_statement.name
91
92    # Update references.
93    for reference in module.FindAll(refactor.Reference):
94      reference_parts = reference.value.split('.')
95      if old_name_parts != reference_parts[:len(old_name_parts)]:
96        continue
97
98      new_reference_parts = [new_name] + reference_parts[len(old_name_parts):]
99      reference.value = '.'.join(new_reference_parts)
100
101    return True
102
103
104def _BaseDir(module_path):
105  if not os.path.isdir(module_path):
106    module_path = os.path.dirname(module_path)
107
108  while '__init__.py' in os.listdir(module_path):
109    module_path = os.path.dirname(module_path)
110
111  return module_path
112
113
114def _ModulePath(module_path):
115  if os.path.split(module_path)[1] == '__init__.py':
116    module_path = os.path.dirname(module_path)
117  rel_path = os.path.relpath(module_path, _BaseDir(module_path))
118  return os.path.splitext(rel_path)[0].replace(os.sep, '.')
119