contrib/hgfixes/fix_leftover_imports.py
changeset 11949 626fe5c99231
child 19378 9de689d20230
equal deleted inserted replaced
11948:88d4911930bf 11949:626fe5c99231
       
     1 "Fixer that translates some APIs ignored by the default 2to3 fixers."
       
     2 
       
     3 # FIXME: This fixer has some ugly hacks. Its main design is based on that of
       
     4 # fix_imports, from lib2to3. Unfortunately, the fix_imports framework only
       
     5 # changes module names "without dots", meaning it won't work for some changes
       
     6 # in the email module/package. Thus this fixer was born. I believe that with a
       
     7 # bit more thinking, a more generic fixer can be implemented, but I'll leave
       
     8 # that as future work.
       
     9 
       
    10 from lib2to3.fixer_util import Name
       
    11 from lib2to3.fixes import fix_imports
       
    12 
       
    13 # This maps the old names to the new names. Note that a drawback of the current
       
    14 # design is that the dictionary keys MUST have EXACTLY one dot (.) in them,
       
    15 # otherwise things will break. (If you don't need a module hierarchy, you're
       
    16 # better of just inherit from fix_imports and overriding the MAPPING dict.)
       
    17 
       
    18 MAPPING = {'email.Utils': 'email.utils',
       
    19            'email.Errors': 'email.errors',
       
    20            'email.Header': 'email.header',
       
    21            'email.Parser': 'email.parser',
       
    22            'email.Encoders': 'email.encoders',
       
    23            'email.MIMEText': 'email.mime.text',
       
    24            'email.MIMEBase': 'email.mime.base',
       
    25            'email.Generator': 'email.generator',
       
    26            'email.MIMEMultipart': 'email.mime.multipart',
       
    27 }
       
    28 
       
    29 def alternates(members):
       
    30     return "(" + "|".join(map(repr, members)) + ")"
       
    31 
       
    32 def build_pattern(mapping=MAPPING):
       
    33     packages = {}
       
    34     for key in mapping:
       
    35         # What we are doing here is the following: with dotted names, we'll
       
    36         # have something like package_name <trailer '.' module>. Then, we are
       
    37         # making a dictionary to copy this structure. For example, if
       
    38         # mapping={'A.B': 'a.b', 'A.C': 'a.c'}, it will generate the dictionary
       
    39         # {'A': ['b', 'c']} to, then, generate something like "A <trailer '.'
       
    40         # ('b' | 'c')".
       
    41         name = key.split('.')
       
    42         prefix = name[0]
       
    43         if prefix in packages:
       
    44             packages[prefix].append(name[1:][0])
       
    45         else:
       
    46             packages[prefix] = name[1:]
       
    47 
       
    48     mod_list = ' | '.join(["'%s' '.' ('%s')" %
       
    49         (key, "' | '".join(packages[key])) for key in packages])
       
    50     mod_list = '(' + mod_list + ' )'
       
    51     bare_names = alternates(mapping.keys())
       
    52 
       
    53     yield """name_import=import_name< 'import' module_name=dotted_name< %s > >
       
    54           """ % mod_list
       
    55 
       
    56     yield """name_import=import_name< 'import'
       
    57             multiple_imports=dotted_as_names< any*
       
    58             module_name=dotted_name< %s >
       
    59             any* >
       
    60             >""" % mod_list
       
    61 
       
    62     packs = ' | '.join(["'%s' trailer<'.' ('%s')>" % (key,
       
    63                "' | '".join(packages[key])) for key in packages])
       
    64 
       
    65     yield "power< package=(%s) trailer<'.' any > any* >" % packs
       
    66 
       
    67 class FixLeftoverImports(fix_imports.FixImports):
       
    68     # We want to run this fixer after fix_import has run (this shouldn't matter
       
    69     # for hg, though, as setup3k prefers to run the default fixers first)
       
    70     mapping = MAPPING
       
    71 
       
    72     def build_pattern(self):
       
    73         return "|".join(build_pattern(self.mapping))
       
    74 
       
    75     def transform(self, node, results):
       
    76         # Mostly copied from fix_imports.py
       
    77         import_mod = results.get("module_name")
       
    78         if import_mod:
       
    79             try:
       
    80                 mod_name = import_mod.value
       
    81             except AttributeError:
       
    82                 # XXX: A hack to remove whitespace prefixes and suffixes
       
    83                 mod_name = str(import_mod).strip()
       
    84             new_name = self.mapping[mod_name]
       
    85             import_mod.replace(Name(new_name, prefix=import_mod.prefix))
       
    86             if "name_import" in results:
       
    87                 # If it's not a "from x import x, y" or "import x as y" import,
       
    88                 # marked its usage to be replaced.
       
    89                 self.replace[mod_name] = new_name
       
    90             if "multiple_imports" in results:
       
    91                 # This is a nasty hack to fix multiple imports on a line (e.g.,
       
    92                 # "import StringIO, urlparse"). The problem is that I can't
       
    93                 # figure out an easy way to make a pattern recognize the keys of
       
    94                 # MAPPING randomly sprinkled in an import statement.
       
    95                 results = self.match(node)
       
    96                 if results:
       
    97                     self.transform(node, results)
       
    98         else:
       
    99             # Replace usage of the module.
       
   100             # Now this is, mostly, a hack
       
   101             bare_name = results["package"][0]
       
   102             bare_name_text = ''.join(map(str, results['package'])).strip()
       
   103             new_name = self.replace.get(bare_name_text)
       
   104             prefix = results['package'][0].prefix
       
   105             if new_name:
       
   106                 bare_name.replace(Name(new_name, prefix=prefix))
       
   107                 results["package"][1].replace(Name(''))
       
   108