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