]>
Commit | Line | Data |
---|---|---|
53e6db90 DC |
1 | import os |
2 | from pathlib import Path | |
3 | from typing import Optional | |
4 | ||
5 | from jedi.inference.cache import inference_state_method_cache | |
6 | from jedi.inference.names import AbstractNameDefinition, ModuleName | |
7 | from jedi.inference.filters import GlobalNameFilter, ParserTreeFilter, DictFilter, MergedFilter | |
8 | from jedi.inference import compiled | |
9 | from jedi.inference.base_value import TreeValue | |
10 | from jedi.inference.names import SubModuleName | |
11 | from jedi.inference.helpers import values_from_qualified_names | |
12 | from jedi.inference.compiled import create_simple_object | |
13 | from jedi.inference.base_value import ValueSet | |
14 | from jedi.inference.context import ModuleContext | |
15 | ||
16 | ||
17 | class _ModuleAttributeName(AbstractNameDefinition): | |
18 | """ | |
19 | For module attributes like __file__, __str__ and so on. | |
20 | """ | |
21 | api_type = 'instance' | |
22 | ||
23 | def __init__(self, parent_module, string_name, string_value=None): | |
24 | self.parent_context = parent_module | |
25 | self.string_name = string_name | |
26 | self._string_value = string_value | |
27 | ||
28 | def infer(self): | |
29 | if self._string_value is not None: | |
30 | s = self._string_value | |
31 | return ValueSet([ | |
32 | create_simple_object(self.parent_context.inference_state, s) | |
33 | ]) | |
34 | return compiled.get_string_value_set(self.parent_context.inference_state) | |
35 | ||
36 | ||
37 | class SubModuleDictMixin: | |
38 | @inference_state_method_cache() | |
39 | def sub_modules_dict(self): | |
40 | """ | |
41 | Lists modules in the directory of this module (if this module is a | |
42 | package). | |
43 | """ | |
44 | names = {} | |
45 | if self.is_package(): | |
46 | mods = self.inference_state.compiled_subprocess.iter_module_names( | |
47 | self.py__path__() | |
48 | ) | |
49 | for name in mods: | |
50 | # It's obviously a relative import to the current module. | |
51 | names[name] = SubModuleName(self.as_context(), name) | |
52 | ||
53 | # In the case of an import like `from x.` we don't need to | |
54 | # add all the variables, this is only about submodules. | |
55 | return names | |
56 | ||
57 | ||
58 | class ModuleMixin(SubModuleDictMixin): | |
59 | _module_name_class = ModuleName | |
60 | ||
61 | def get_filters(self, origin_scope=None): | |
62 | yield MergedFilter( | |
63 | ParserTreeFilter( | |
64 | parent_context=self.as_context(), | |
65 | origin_scope=origin_scope | |
66 | ), | |
67 | GlobalNameFilter(self.as_context()), | |
68 | ) | |
69 | yield DictFilter(self.sub_modules_dict()) | |
70 | yield DictFilter(self._module_attributes_dict()) | |
71 | yield from self.iter_star_filters() | |
72 | ||
73 | def py__class__(self): | |
74 | c, = values_from_qualified_names(self.inference_state, 'types', 'ModuleType') | |
75 | return c | |
76 | ||
77 | def is_module(self): | |
78 | return True | |
79 | ||
80 | def is_stub(self): | |
81 | return False | |
82 | ||
83 | @property # type: ignore[misc] | |
84 | @inference_state_method_cache() | |
85 | def name(self): | |
86 | return self._module_name_class(self, self.string_names[-1]) | |
87 | ||
88 | @inference_state_method_cache() | |
89 | def _module_attributes_dict(self): | |
90 | names = ['__package__', '__doc__', '__name__'] | |
91 | # All the additional module attributes are strings. | |
92 | dct = dict((n, _ModuleAttributeName(self, n)) for n in names) | |
93 | path = self.py__file__() | |
94 | if path is not None: | |
95 | dct['__file__'] = _ModuleAttributeName(self, '__file__', str(path)) | |
96 | return dct | |
97 | ||
98 | def iter_star_filters(self): | |
99 | for star_module in self.star_imports(): | |
100 | f = next(star_module.get_filters(), None) | |
101 | assert f is not None | |
102 | yield f | |
103 | ||
104 | # I'm not sure if the star import cache is really that effective anymore | |
105 | # with all the other really fast import caches. Recheck. Also we would need | |
106 | # to push the star imports into InferenceState.module_cache, if we reenable this. | |
107 | @inference_state_method_cache([]) | |
108 | def star_imports(self): | |
109 | from jedi.inference.imports import Importer | |
110 | ||
111 | modules = [] | |
112 | module_context = self.as_context() | |
113 | for i in self.tree_node.iter_imports(): | |
114 | if i.is_star_import(): | |
115 | new = Importer( | |
116 | self.inference_state, | |
117 | import_path=i.get_paths()[-1], | |
118 | module_context=module_context, | |
119 | level=i.level | |
120 | ).follow() | |
121 | ||
122 | for module in new: | |
123 | if isinstance(module, ModuleValue): | |
124 | modules += module.star_imports() | |
125 | modules += new | |
126 | return modules | |
127 | ||
128 | def get_qualified_names(self): | |
129 | """ | |
130 | A module doesn't have a qualified name, but it's important to note that | |
131 | it's reachable and not `None`. With this information we can add | |
132 | qualified names on top for all value children. | |
133 | """ | |
134 | return () | |
135 | ||
136 | ||
137 | class ModuleValue(ModuleMixin, TreeValue): | |
138 | api_type = 'module' | |
139 | ||
140 | def __init__(self, inference_state, module_node, code_lines, file_io=None, | |
141 | string_names=None, is_package=False): | |
142 | super().__init__( | |
143 | inference_state, | |
144 | parent_context=None, | |
145 | tree_node=module_node | |
146 | ) | |
147 | self.file_io = file_io | |
148 | if file_io is None: | |
149 | self._path: Optional[Path] = None | |
150 | else: | |
151 | self._path = file_io.path | |
152 | self.string_names = string_names # Optional[Tuple[str, ...]] | |
153 | self.code_lines = code_lines | |
154 | self._is_package = is_package | |
155 | ||
156 | def is_stub(self): | |
157 | if self._path is not None and self._path.suffix == '.pyi': | |
158 | # Currently this is the way how we identify stubs when e.g. goto is | |
159 | # used in them. This could be changed if stubs would be identified | |
160 | # sooner and used as StubModuleValue. | |
161 | return True | |
162 | return super().is_stub() | |
163 | ||
164 | def py__name__(self): | |
165 | if self.string_names is None: | |
166 | return None | |
167 | return '.'.join(self.string_names) | |
168 | ||
169 | def py__file__(self) -> Optional[Path]: | |
170 | """ | |
171 | In contrast to Python's __file__ can be None. | |
172 | """ | |
173 | if self._path is None: | |
174 | return None | |
175 | ||
176 | return self._path.absolute() | |
177 | ||
178 | def is_package(self): | |
179 | return self._is_package | |
180 | ||
181 | def py__package__(self): | |
182 | if self.string_names is None: | |
183 | return [] | |
184 | ||
185 | if self._is_package: | |
186 | return self.string_names | |
187 | return self.string_names[:-1] | |
188 | ||
189 | def py__path__(self): | |
190 | """ | |
191 | In case of a package, this returns Python's __path__ attribute, which | |
192 | is a list of paths (strings). | |
193 | Returns None if the module is not a package. | |
194 | """ | |
195 | if not self._is_package: | |
196 | return None | |
197 | ||
198 | # A namespace package is typically auto generated and ~10 lines long. | |
199 | first_few_lines = ''.join(self.code_lines[:50]) | |
200 | # these are strings that need to be used for namespace packages, | |
201 | # the first one is ``pkgutil``, the second ``pkg_resources``. | |
202 | options = ('declare_namespace(__name__)', 'extend_path(__path__') | |
203 | if options[0] in first_few_lines or options[1] in first_few_lines: | |
204 | # It is a namespace, now try to find the rest of the | |
205 | # modules on sys_path or whatever the search_path is. | |
206 | paths = set() | |
207 | for s in self.inference_state.get_sys_path(): | |
208 | other = os.path.join(s, self.name.string_name) | |
209 | if os.path.isdir(other): | |
210 | paths.add(other) | |
211 | if paths: | |
212 | return list(paths) | |
213 | # Nested namespace packages will not be supported. Nobody ever | |
214 | # asked for it and in Python 3 they are there without using all the | |
215 | # crap above. | |
216 | ||
217 | # Default to the of this file. | |
218 | file = self.py__file__() | |
219 | assert file is not None # Shouldn't be a package in the first place. | |
220 | return [os.path.dirname(file)] | |
221 | ||
222 | def _as_context(self): | |
223 | return ModuleContext(self) | |
224 | ||
225 | def __repr__(self): | |
226 | return "<%s: %s@%s-%s is_stub=%s>" % ( | |
227 | self.__class__.__name__, self.py__name__(), | |
228 | self.tree_node.start_pos[0], self.tree_node.end_pos[0], | |
229 | self.is_stub() | |
230 | ) |