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