1# Copyright 2017 Google Inc.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Logic for resolving import paths."""
16
17import collections
18import logging
19import sys
20
21from . import import_finder
22from . import utils
23
24
25class ParseError(Exception):
26    """Error parsing a file with python."""
27    pass
28
29
30class ImportStatement(collections.namedtuple(
31        'ImportStatement',
32        ['name', 'new_name', 'is_from', 'is_star', 'source'])):
33    """A Python import statement, such as "import foo as bar"."""
34
35    def __new__(cls, name, new_name=None, is_from=False, is_star=False,
36                source=None):
37        """Create a new ImportStatement.
38
39        Args:
40          name: Name of the module to be imported. E.g. "sys".
41          new_name: What the module is renamed to. The "y" in
42            "import x as y".
43          is_from: If the last part of the name (the "z" in "x.y.z") can
44          be an element within a module, instead of a module itself. Happens
45          e.g. for "from sys import argv".
46          is_star: If this is an import of the form "from x import *".
47          source: The path to the file as resolved by python.
48        Returns:
49          A new ImportStatement instance.
50        """
51        return super(ImportStatement, cls).__new__(
52            cls, name, new_name or name, is_from, is_star, source)
53
54    def is_relative(self):
55        return self.name.startswith('.')
56
57    def __str__(self):
58        if self.is_star:
59            assert self.name == self.new_name
60            assert self.is_from
61            return 'from ' + self.name + ' import *'
62        if self.is_from:
63            try:
64                left, right = self.name.rsplit('.', 2)
65            except ValueError:
66                left, right = self.name, ''
67            module = left + '[.' + right + ']'
68        else:
69            module = self.name
70        if self.new_name != self.name:
71            return 'import ' + module + ' as ' + self.new_name
72        else:
73            return 'import ' + module
74
75
76def get_imports(filename, python_version):
77    if python_version == sys.version_info[0:2]:
78        # Invoke import_finder directly
79        try:
80            imports = import_finder.get_imports(filename)
81        except Exception:
82            raise ParseError(filename)
83    else:
84        # Call the appropriate python version in a subprocess
85        f = sys.modules['importlab.import_finder'].__file__
86        if f.rsplit('.', 1)[-1] == 'pyc':
87            # In host Python 2, importlab ships with .pyc files.
88            f = f[:-1]
89        ret, stdout, stderr = utils.run_py_file(python_version, f, filename)
90        if not ret:
91            if sys.version_info[0] == 3:
92                stdout = stdout.decode('ascii')
93            imports = import_finder.read_imports(stdout)
94        else:
95            if sys.version_info[0] == 3:
96                stderr = stderr.decode('ascii')
97            logging.info(stderr)
98            raise ParseError(filename)
99    return [ImportStatement(*imp) for imp in imports]
100