Home | History | Annotate | Download | only in annotated_symbol
      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 
      5 import itertools
      6 import keyword
      7 import symbol
      8 import token
      9 
     10 from py_utils.refactor.annotated_symbol import base_symbol
     11 from py_utils.refactor import snippet
     12 
     13 
     14 __all__ = [
     15     'AsName',
     16     'DottedName',
     17     'Import',
     18     'ImportFrom',
     19     'ImportName',
     20 ]
     21 
     22 
     23 class DottedName(base_symbol.AnnotatedSymbol):
     24   @classmethod
     25   def Annotate(cls, symbol_type, children):
     26     if symbol_type != symbol.dotted_name:
     27       return None
     28     return cls(symbol_type, children)
     29 
     30   @property
     31   def value(self):
     32     return ''.join(token_snippet.value for token_snippet in self._children)
     33 
     34   @value.setter
     35   def value(self, value):
     36     value_parts = value.split('.')
     37     for value_part in value_parts:
     38       if keyword.iskeyword(value_part):
     39         raise ValueError('%s is a reserved keyword.' % value_part)
     40 
     41     # If we have too many children, cut the list down to size.
     42     self._children = self._children[:len(value_parts)*2-1]
     43 
     44     # Update child nodes.
     45     for child, value_part in itertools.izip_longest(
     46         self._children[::2], value_parts):
     47       if child:
     48         # Modify existing children. This helps preserve comments and spaces.
     49         child.value = value_part
     50       else:
     51         # Add children as needed.
     52         self._children.append(snippet.TokenSnippet.Create(token.DOT, '.'))
     53         self._children.append(
     54             snippet.TokenSnippet.Create(token.NAME, value_part))
     55 
     56 
     57 class AsName(base_symbol.AnnotatedSymbol):
     58   @classmethod
     59   def Annotate(cls, symbol_type, children):
     60     if (symbol_type != symbol.dotted_as_name and
     61         symbol_type != symbol.import_as_name):
     62       return None
     63     return cls(symbol_type, children)
     64 
     65   @property
     66   def name(self):
     67     return self.children[0].value
     68 
     69   @name.setter
     70   def name(self, value):
     71     self.children[0].value = value
     72 
     73   @property
     74   def alias(self):
     75     if len(self.children) < 3:
     76       return None
     77     return self.children[2].value
     78 
     79   @alias.setter
     80   def alias(self, value):
     81     if keyword.iskeyword(value):
     82       raise ValueError('%s is a reserved keyword.' % value)
     83 
     84     if value:
     85       if len(self.children) < 3:
     86         # If we currently have no alias, add one.
     87         self.children.append(
     88             snippet.TokenSnippet.Create(token.NAME, 'as', (0, 1)))
     89         self.children.append(
     90             snippet.TokenSnippet.Create(token.NAME, value, (0, 1)))
     91       else:
     92         # We already have an alias. Just update the value.
     93         self.children[2].value = value
     94     else:
     95       # Removing the alias. Strip the "as foo".
     96       self.children = [self.children[0]]
     97 
     98 
     99 class Import(base_symbol.AnnotatedSymbol):
    100   """An import statement.
    101 
    102   Example:
    103     import a.b.c as d
    104     from a.b import c as d
    105 
    106   In these examples,
    107     path == 'a.b.c'
    108     alias == 'd'
    109     root == 'a.b' (only for "from" imports)
    110     module == 'c' (only for "from" imports)
    111     name (read-only) == the name used by references to the module, which is the
    112     alias if there is one, the full module path in "full" imports, and the
    113     module name in "from" imports.
    114   """
    115   @property
    116   def has_from(self):
    117     """Returns True iff the import statment is of the form "from x import y"."""
    118     raise NotImplementedError()
    119 
    120   @property
    121   def values(self):
    122     raise NotImplementedError()
    123 
    124   @property
    125   def paths(self):
    126     raise NotImplementedError()
    127 
    128   @property
    129   def aliases(self):
    130     raise NotImplementedError()
    131 
    132   @property
    133   def path(self):
    134     """The full dotted path of the module."""
    135     raise NotImplementedError()
    136 
    137   @path.setter
    138   def path(self, value):
    139     raise NotImplementedError()
    140 
    141   @property
    142   def alias(self):
    143     """The alias, if the module is renamed with "as". None otherwise."""
    144     raise NotImplementedError()
    145 
    146   @alias.setter
    147   def alias(self, value):
    148     raise NotImplementedError()
    149 
    150   @property
    151   def name(self):
    152     """The name used to reference this import's module."""
    153     raise NotImplementedError()
    154 
    155 
    156 class ImportName(Import):
    157   @classmethod
    158   def Annotate(cls, symbol_type, children):
    159     if symbol_type != symbol.import_stmt:
    160       return None
    161     if children[0].type != symbol.import_name:
    162       return None
    163     assert len(children) == 1
    164     return cls(symbol_type, children[0].children)
    165 
    166   @property
    167   def has_from(self):
    168     return False
    169 
    170   @property
    171   def values(self):
    172     dotted_as_names = self.children[1]
    173     return tuple((dotted_as_name.name, dotted_as_name.alias)
    174                  for dotted_as_name in dotted_as_names.children[::2])
    175 
    176   @property
    177   def paths(self):
    178     return tuple(path for path, _ in self.values)
    179 
    180   @property
    181   def aliases(self):
    182     return tuple(alias for _, alias in self.values)
    183 
    184   @property
    185   def _dotted_as_name(self):
    186     dotted_as_names = self.children[1]
    187     if len(dotted_as_names.children) != 1:
    188       raise NotImplementedError(
    189           'This method only works if the statement has one import.')
    190     return dotted_as_names.children[0]
    191 
    192   @property
    193   def path(self):
    194     return self._dotted_as_name.name
    195 
    196   @path.setter
    197   def path(self, value):  # pylint: disable=arguments-differ
    198     self._dotted_as_name.name = value
    199 
    200   @property
    201   def alias(self):
    202     return self._dotted_as_name.alias
    203 
    204   @alias.setter
    205   def alias(self, value):  # pylint: disable=arguments-differ
    206     self._dotted_as_name.alias = value
    207 
    208   @property
    209   def name(self):
    210     if self.alias:
    211       return self.alias
    212     else:
    213       return self.path
    214 
    215 
    216 class ImportFrom(Import):
    217   @classmethod
    218   def Annotate(cls, symbol_type, children):
    219     if symbol_type != symbol.import_stmt:
    220       return None
    221     if children[0].type != symbol.import_from:
    222       return None
    223     assert len(children) == 1
    224     return cls(symbol_type, children[0].children)
    225 
    226   @property
    227   def has_from(self):
    228     return True
    229 
    230   @property
    231   def values(self):
    232     try:
    233       import_as_names = self.FindChild(symbol.import_as_names)
    234     except ValueError:
    235       return (('*', None),)
    236 
    237     return tuple((import_as_name.name, import_as_name.alias)
    238                  for import_as_name in import_as_names.children[::2])
    239 
    240   @property
    241   def paths(self):
    242     module = self.module
    243     return tuple('.'.join((module, name)) for name, _ in self.values)
    244 
    245   @property
    246   def aliases(self):
    247     return tuple(alias for _, alias in self.values)
    248 
    249   @property
    250   def root(self):
    251     return self.FindChild(symbol.dotted_name).value
    252 
    253   @root.setter
    254   def root(self, value):
    255     self.FindChild(symbol.dotted_name).value = value
    256 
    257   @property
    258   def _import_as_name(self):
    259     try:
    260       import_as_names = self.FindChild(symbol.import_as_names)
    261     except ValueError:
    262       return None
    263 
    264     if len(import_as_names.children) != 1:
    265       raise NotImplementedError(
    266           'This method only works if the statement has one import.')
    267 
    268     return import_as_names.children[0]
    269 
    270   @property
    271   def module(self):
    272     import_as_name = self._import_as_name
    273     if import_as_name:
    274       return import_as_name.name
    275     else:
    276       return '*'
    277 
    278   @module.setter
    279   def module(self, value):
    280     if keyword.iskeyword(value):
    281       raise ValueError('%s is a reserved keyword.' % value)
    282 
    283     import_as_name = self._import_as_name
    284     if value == '*':
    285       # TODO: Implement this.
    286       raise NotImplementedError()
    287     else:
    288       if import_as_name:
    289         import_as_name.name = value
    290       else:
    291         # TODO: Implement this.
    292         raise NotImplementedError()
    293 
    294   @property
    295   def path(self):
    296     return '.'.join((self.root, self.module))
    297 
    298   @path.setter
    299   def path(self, value):  # pylint: disable=arguments-differ
    300     self.root, _, self.module = value.rpartition('.')
    301 
    302   @property
    303   def alias(self):
    304     import_as_name = self._import_as_name
    305     if import_as_name:
    306       return import_as_name.alias
    307     else:
    308       return None
    309 
    310   @alias.setter
    311   def alias(self, value):  # pylint: disable=arguments-differ
    312     import_as_name = self._import_as_name
    313     if not import_as_name:
    314       raise NotImplementedError('Cannot change alias for "import *".')
    315     import_as_name.alias = value
    316 
    317   @property
    318   def name(self):
    319     if self.alias:
    320       return self.alias
    321     else:
    322       return self.module
    323