3 from pathlib
import Path
4 from importlib
.machinery
import all_suffixes
6 from jedi
.inference
.cache
import inference_state_method_cache
7 from jedi
.inference
.base_value
import ContextualizedNode
8 from jedi
.inference
.helpers
import is_string
, get_str_or_none
9 from jedi
.parser_utils
import get_cached_code_lines
10 from jedi
.file_io
import FileIO
11 from jedi
import settings
12 from jedi
import debug
14 _BUILDOUT_PATH_INSERTION_LIMIT
= 10
17 def _abs_path(module_context
, str_path
: str):
19 if path
.is_absolute():
22 module_path
= module_context
.py__file__()
23 if module_path
is None:
24 # In this case we have no idea where we actually are in the file
28 base_dir
= module_path
.parent
29 return base_dir
.joinpath(path
).absolute()
32 def _paths_from_assignment(module_context
, expr_stmt
):
34 Extracts the assigned strings from an assignment that looks as follows::
36 sys.path[0:0] = ['module/path', 'another/module/path']
38 This function is in general pretty tolerant (and therefore 'buggy').
39 However, it's not a big issue usually to add more paths to Jedi's sys_path,
40 because it will only affect Jedi in very random situations and by adding
41 more paths than necessary, it usually benefits the general user.
43 for assignee
, operator
in zip(expr_stmt
.children
[::2], expr_stmt
.children
[1::2]):
45 assert operator
in ['=', '+=']
46 assert assignee
.type in ('power', 'atom_expr') and \
47 len(assignee
.children
) > 1
49 assert c
[0].type == 'name' and c
[0].value
== 'sys'
51 assert trailer
.children
[0] == '.' and trailer
.children
[1].value
== 'path'
52 # TODO Essentially we're not checking details on sys.path
53 # manipulation. Both assigment of the sys.path and changing/adding
54 # parts of the sys.path are the same: They get added to the end of
55 # the current sys.path.
58 assert execution.children[0] == '['
59 subscript = execution.children[1]
60 assert subscript.type == 'subscript'
61 assert ':' in subscript.children
63 except AssertionError:
66 cn
= ContextualizedNode(module_context
.create_context(expr_stmt
), expr_stmt
)
67 for lazy_value
in cn
.infer().iterate(cn
):
68 for value
in lazy_value
.infer():
70 abs_path
= _abs_path(module_context
, value
.get_safe_value())
71 if abs_path
is not None:
75 def _paths_from_list_modifications(module_context
, trailer1
, trailer2
):
76 """ extract the path from either "sys.path.append" or "sys.path.insert" """
77 # Guarantee that both are trailers, the first one a name and the second one
78 # a function execution with at least one param.
79 if not (trailer1
.type == 'trailer' and trailer1
.children
[0] == '.'
80 and trailer2
.type == 'trailer' and trailer2
.children
[0] == '('
81 and len(trailer2
.children
) == 3):
84 name
= trailer1
.children
[1].value
85 if name
not in ['insert', 'append']:
87 arg
= trailer2
.children
[1]
88 if name
== 'insert' and len(arg
.children
) in (3, 4): # Possible trailing comma.
91 for value
in module_context
.create_context(arg
).infer_node(arg
):
92 p
= get_str_or_none(value
)
95 abs_path
= _abs_path(module_context
, p
)
96 if abs_path
is not None:
100 @inference_state_method_cache(default
=[])
101 def check_sys_path_modifications(module_context
):
103 Detect sys.path modifications within module.
105 def get_sys_path_powers(names
):
107 power
= name
.parent
.parent
108 if power
is not None and power
.type in ('power', 'atom_expr'):
110 if c
[0].type == 'name' and c
[0].value
== 'sys' \
111 and c
[1].type == 'trailer':
113 if n
.type == 'name' and n
.value
== 'path':
116 if module_context
.tree_node
is None:
121 possible_names
= module_context
.tree_node
.get_used_names()['path']
125 for name
, power
in get_sys_path_powers(possible_names
):
126 expr_stmt
= power
.parent
127 if len(power
.children
) >= 4:
129 _paths_from_list_modifications(
130 module_context
, *power
.children
[2:4]
133 elif expr_stmt
is not None and expr_stmt
.type == 'expr_stmt':
134 added
.extend(_paths_from_assignment(module_context
, expr_stmt
))
138 def discover_buildout_paths(inference_state
, script_path
):
139 buildout_script_paths
= set()
141 for buildout_script_path
in _get_buildout_script_paths(script_path
):
142 for path
in _get_paths_from_buildout_script(inference_state
, buildout_script_path
):
143 buildout_script_paths
.add(path
)
144 if len(buildout_script_paths
) >= _BUILDOUT_PATH_INSERTION_LIMIT
:
147 return buildout_script_paths
150 def _get_paths_from_buildout_script(inference_state
, buildout_script_path
):
151 file_io
= FileIO(str(buildout_script_path
))
153 module_node
= inference_state
.parse(
156 cache_path
=settings
.cache_directory
159 debug
.warning('Error trying to read buildout_script: %s', buildout_script_path
)
162 from jedi
.inference
.value
import ModuleValue
163 module_context
= ModuleValue(
164 inference_state
, module_node
,
167 code_lines
=get_cached_code_lines(inference_state
.grammar
, buildout_script_path
),
169 yield from check_sys_path_modifications(module_context
)
172 def _get_parent_dir_with_file(path
: Path
, filename
):
173 for parent
in path
.parents
:
175 if parent
.joinpath(filename
).is_file():
182 def _get_buildout_script_paths(search_path
: Path
):
184 if there is a 'buildout.cfg' file in one of the parent directories of the
185 given module it will return a list of all files in the buildout bin
186 directory that look like python files.
188 :param search_path: absolute path to the module.
190 project_root
= _get_parent_dir_with_file(search_path
, 'buildout.cfg')
193 bin_path
= project_root
.joinpath('bin')
194 if not bin_path
.exists():
197 for filename
in os
.listdir(bin_path
):
199 filepath
= bin_path
.joinpath(filename
)
200 with
open(filepath
, 'r') as f
:
201 firstline
= f
.readline()
202 if firstline
.startswith('#!') and 'python' in firstline
:
204 except (UnicodeDecodeError, IOError) as e
:
205 # Probably a binary file; permission error or race cond. because
206 # file got deleted. Ignore it.
207 debug
.warning(str(e
))
211 def remove_python_path_suffix(path
):
212 for suffix
in all_suffixes() + ['.pyi']:
213 if path
.suffix
== suffix
:
214 path
= path
.with_name(path
.stem
)
219 def transform_path_to_dotted(sys_path
, module_path
):
221 Returns the dotted path inside a sys.path as a list of names. e.g.
223 >>> transform_path_to_dotted([str(Path("/foo").absolute())], Path('/foo/bar/baz.py').absolute())
224 (('bar', 'baz'), False)
226 Returns (None, False) if the path doesn't really resolve to anything.
227 The second return part is if it is a package.
229 # First remove the suffix.
230 module_path
= remove_python_path_suffix(module_path
)
231 if module_path
.name
.startswith('.'):
234 # Once the suffix was removed we are using the files as we know them. This
235 # means that if someone uses an ending like .vim for a Python file, .vim
236 # will be part of the returned dotted part.
238 is_package
= module_path
.name
== '__init__'
240 module_path
= module_path
.parent
242 def iter_potential_solutions():
244 if str(module_path
).startswith(p
):
245 # Strip the trailing slash/backslash
246 rest
= str(module_path
)[len(p
):]
247 # On Windows a path can also use a slash.
248 if rest
.startswith(os
.path
.sep
) or rest
.startswith('/'):
249 # Remove a slash in cases it's still there.
253 split
= rest
.split(os
.path
.sep
)
255 # This means that part of the file path was empty, this
256 # is very strange and is probably a file that is called
259 # Stub folders for foo can end with foo-stubs. Just remove
261 yield tuple(re
.sub(r
'-stubs$', '', s
) for s
in split
)
263 potential_solutions
= tuple(iter_potential_solutions())
264 if not potential_solutions
:
266 # Try to find the shortest path, this makes more sense usually, because the
267 # user usually has venvs somewhere. This means that a path like
268 # .tox/py37/lib/python3.7/os.py can be normal for a file. However in that
269 # case we definitely want to return ['os'] as a path and not a crazy
270 # ['.tox', 'py37', 'lib', 'python3.7', 'os']. Keep in mind that this is a
271 # heuristic and there's now ay to "always" do it right.
272 return sorted(potential_solutions
, key
=lambda p
: len(p
))[0], is_package