2 Projects are a way to handle Python projects within Jedi. For simpler plugins
3 you might not want to deal with projects, but if you want to give the user more
4 flexibility to define sys paths and Python interpreters for a project,
5 :class:`.Project` is the perfect way to allow for that.
7 Projects can be saved to disk and loaded again, to allow project definitions to
8 be used across repositories.
11 from pathlib
import Path
12 from itertools
import chain
14 from jedi
import debug
15 from jedi
.api
.environment
import get_cached_default_environment
, create_environment
16 from jedi
.api
.exceptions
import WrongVersion
17 from jedi
.api
.completion
import search_in_module
18 from jedi
.api
.helpers
import split_search_string
, get_module_names
19 from jedi
.inference
.imports
import load_module_from_path
, \
20 load_namespace_from_path
, iter_module_names
21 from jedi
.inference
.sys_path
import discover_buildout_paths
22 from jedi
.inference
.cache
import inference_state_as_method_param_cache
23 from jedi
.inference
.references
import recurse_find_python_folders_and_files
, search_in_file_ios
24 from jedi
.file_io
import FolderIO
26 _CONFIG_FOLDER
= '.jedi'
27 _CONTAINS_POTENTIAL_PROJECT
= \
28 'setup.py', '.git', '.hg', 'requirements.txt', 'MANIFEST.in', 'pyproject.toml'
30 _SERIALIZER_VERSION
= 1
33 def _try_to_skip_duplicates(func
):
34 def wrapper(*args
, **kwargs
):
37 for definition
in func(*args
, **kwargs
):
38 tree_node
= definition
._name
.tree_name
39 if tree_node
is not None and tree_node
in found_tree_nodes
:
41 if definition
.type == 'module' and definition
.module_path
is not None:
42 if definition
.module_path
in found_modules
:
44 found_modules
.append(definition
.module_path
)
46 found_tree_nodes
.append(tree_node
)
50 def _remove_duplicates_from_path(path
):
61 Projects are a simple way to manage Python folders and define how Jedi does
62 import resolution. It is mostly used as a parameter to :class:`.Script`.
63 Additionally there are functions to search a whole project.
68 def _get_config_folder_path(base_path
):
69 return base_path
.joinpath(_CONFIG_FOLDER
)
72 def _get_json_path(base_path
):
73 return Project
._get
_config
_folder
_path
(base_path
).joinpath('project.json')
78 Loads a project from a specific path. You should not provide the path
79 to ``.jedi/project.json``, but rather the path to the project folder.
81 :param path: The path of the directory you want to use as a project.
83 if isinstance(path
, str):
85 with
open(cls
._get
_json
_path
(path
)) as f
:
86 version
, data
= json
.load(f
)
92 "The Jedi version of this project seems newer than what we can handle."
97 Saves the project configuration in the project in ``.jedi/project.json``.
99 data
= dict(self
.__dict
__)
100 data
.pop('_environment', None)
101 data
.pop('_django', None) # TODO make django setting public?
102 data
= {k
.lstrip('_'): v
for k
, v
in data
.items()}
103 data
['path'] = str(data
['path'])
105 self
._get
_config
_folder
_path
(self
._path
).mkdir(parents
=True, exist_ok
=True)
106 with
open(self
._get
_json
_path
(self
._path
), 'w') as f
:
107 return json
.dump((_SERIALIZER_VERSION
, data
), f
)
113 environment_path
=None,
114 load_unsafe_extensions
=False,
120 :param path: The base path for this project.
121 :param environment_path: The Python executable path, typically the path
122 of a virtual environment.
123 :param load_unsafe_extensions: Default False, Loads extensions that are not in the
124 sys path and in the local directories. With this option enabled,
125 this is potentially unsafe if you clone a git repository and
126 analyze it's code, because those compiled extensions will be
127 important and therefore have execution privileges.
128 :param sys_path: list of str. You can override the sys path if you
129 want. By default the ``sys.path.`` is generated by the
130 environment (virtualenvs, etc).
131 :param added_sys_path: list of str. Adds these paths at the end of the
133 :param smart_sys_path: If this is enabled (default), adds paths from
134 local directories. Otherwise you will have to rely on your packages
135 being properly configured on the ``sys.path``.
138 if isinstance(path
, str):
139 path
= Path(path
).absolute()
142 self
._environment
_path
= environment_path
143 if sys_path
is not None:
144 # Remap potential pathlib.Path entries
145 sys_path
= list(map(str, sys_path
))
146 self
._sys
_path
= sys_path
147 self
._smart
_sys
_path
= smart_sys_path
148 self
._load
_unsafe
_extensions
= load_unsafe_extensions
150 # Remap potential pathlib.Path entries
151 self
.added_sys_path
= list(map(str, added_sys_path
))
152 """The sys path that is going to be added at the end of the """
157 The base path for this project.
164 The sys path provided to this project. This can be None and in that
165 case will be auto generated.
167 return self
._sys
_path
170 def smart_sys_path(self
):
172 If the sys path is going to be calculated in a smart way, where
173 additional paths are added.
175 return self
._smart
_sys
_path
178 def load_unsafe_extensions(self
):
180 Wheter the project loads unsafe extensions.
182 return self
._load
_unsafe
_extensions
184 @inference_state_as_method_param_cache()
185 def _get_base_sys_path(self
, inference_state
):
186 # The sys path has not been set explicitly.
187 sys_path
= list(inference_state
.environment
.get_sys_path())
194 @inference_state_as_method_param_cache()
195 def _get_sys_path(self
, inference_state
, add_parent_paths
=True, add_init_paths
=False):
197 Keep this method private for all users of jedi. However internally this
198 one is used like a public method.
200 suffixed
= list(self
.added_sys_path
)
203 if self
._sys
_path
is None:
204 sys_path
= list(self
._get
_base
_sys
_path
(inference_state
))
206 sys_path
= list(self
._sys
_path
)
208 if self
._smart
_sys
_path
:
209 prefixed
.append(str(self
._path
))
211 if inference_state
.script_path
is not None:
212 suffixed
+= map(str, discover_buildout_paths(
214 inference_state
.script_path
218 # Collect directories in upward search by:
219 # 1. Skipping directories with __init__.py
220 # 2. Stopping immediately when above self._path
222 for parent_path
in inference_state
.script_path
.parents
:
223 if parent_path
== self
._path \
224 or self
._path
not in parent_path
.parents
:
226 if not add_init_paths \
227 and parent_path
.joinpath("__init__.py").is_file():
229 traversed
.append(str(parent_path
))
231 # AFAIK some libraries have imports like `foo.foo.bar`, which
232 # leads to the conclusion to by default prefer longer paths
233 # rather than shorter ones by default.
234 suffixed
+= reversed(traversed
)
237 prefixed
.append(str(self
._path
))
239 path
= prefixed
+ sys_path
+ suffixed
240 return list(_remove_duplicates_from_path(path
))
242 def get_environment(self
):
243 if self
._environment
is None:
244 if self
._environment
_path
is not None:
245 self
._environment
= create_environment(self
._environment
_path
, safe
=False)
247 self
._environment
= get_cached_default_environment()
248 return self
._environment
250 def search(self
, string
, *, all_scopes
=False):
252 Searches a name in the whole project. If the project is very big,
253 at some point Jedi will stop searching. However it's also very much
254 recommended to not exhaust the generator. Just display the first ten
257 There are currently three different search patterns:
259 - ``foo`` to search for a definition foo in any file or a file called
260 ``foo.py`` or ``foo.pyi``.
261 - ``foo.bar`` to search for the ``foo`` and then an attribute ``bar``
263 - ``class foo.bar.Bar`` or ``def foo.bar.baz`` to search for a specific
266 :param bool all_scopes: Default False; searches not only for
267 definitions on the top level of a module level, but also in
268 functions and classes.
269 :yields: :class:`.Name`
271 return self
._search
_func
(string
, all_scopes
=all_scopes
)
273 def complete_search(self
, string
, **kwargs
):
275 Like :meth:`.Script.search`, but completes that string. An empty string
276 lists all definitions in a project, so be careful with that.
278 :param bool all_scopes: Default False; searches not only for
279 definitions on the top level of a module level, but also in
280 functions and classes.
281 :yields: :class:`.Completion`
283 return self
._search
_func
(string
, complete
=True, **kwargs
)
285 @_try_to_skip_duplicates
286 def _search_func(self
, string
, complete
=False, all_scopes
=False):
287 # Using a Script is they easiest way to get an empty module context.
288 from jedi
import Script
289 s
= Script('', project
=self
)
290 inference_state
= s
._inference
_state
291 empty_module_context
= s
._get
_module
_context
()
293 debug
.dbg('Search for string %s, complete=%s', string
, complete
)
294 wanted_type
, wanted_names
= split_search_string(string
)
295 name
= wanted_names
[0]
296 stub_folder_name
= name
+ '-stubs'
298 ios
= recurse_find_python_folders_and_files(FolderIO(str(self
._path
)))
301 # 1. Search for modules in the current project
302 for folder_io
, file_io
in ios
:
304 file_name
= folder_io
.get_base_name()
305 if file_name
== name
or file_name
== stub_folder_name
:
306 f
= folder_io
.get_file_io('__init__.py')
308 m
= load_module_from_path(inference_state
, f
).as_context()
309 except FileNotFoundError
:
310 f
= folder_io
.get_file_io('__init__.pyi')
312 m
= load_module_from_path(inference_state
, f
).as_context()
313 except FileNotFoundError
:
314 m
= load_namespace_from_path(inference_state
, folder_io
).as_context()
318 file_ios
.append(file_io
)
319 if Path(file_io
.path
).name
in (name
+ '.py', name
+ '.pyi'):
320 m
= load_module_from_path(inference_state
, file_io
).as_context()
324 debug
.dbg('Search of a specific module %s', m
)
325 yield from search_in_module(
329 wanted_type
=wanted_type
,
330 wanted_names
=wanted_names
,
336 # 2. Search for identifiers in the project.
337 for module_context
in search_in_file_ios(inference_state
, file_ios
,
338 name
, complete
=complete
):
339 names
= get_module_names(module_context
.tree_node
, all_scopes
=all_scopes
)
340 names
= [module_context
.create_name(n
) for n
in names
]
341 names
= _remove_imports(names
)
342 yield from search_in_module(
346 wanted_type
=wanted_type
,
347 wanted_names
=wanted_names
,
352 # 3. Search for modules on sys.path
354 p
for p
in self
._get
_sys
_path
(inference_state
)
355 # Exclude the current folder which is handled by recursing the folders.
358 names
= list(iter_module_names(inference_state
, empty_module_context
, sys_path
))
359 yield from search_in_module(
361 empty_module_context
,
363 wanted_type
=wanted_type
,
364 wanted_names
=wanted_names
,
370 return '<%s: %s>' % (self
.__class
__.__name
__, self
._path
)
373 def _is_potential_project(path
):
374 for name
in _CONTAINS_POTENTIAL_PROJECT
:
376 if path
.joinpath(name
).exists():
383 def _is_django_path(directory
):
384 """ Detects the path of the very well known Django library (if used) """
386 with
open(directory
.joinpath('manage.py'), 'rb') as f
:
387 return b
"DJANGO_SETTINGS_MODULE" in f
.read()
388 except (FileNotFoundError
, IsADirectoryError
, PermissionError
):
392 def get_default_project(path
=None):
394 If a project is not defined by the user, Jedi tries to define a project by
395 itself as well as possible. Jedi traverses folders until it finds one of
398 1. A ``.jedi/config.json``
399 2. One of the following files: ``setup.py``, ``.git``, ``.hg``,
400 ``requirements.txt`` and ``MANIFEST.in``.
404 elif isinstance(path
, str):
407 check
= path
.absolute()
409 first_no_init_file
= None
410 for dir in chain([check
], check
.parents
):
412 return Project
.load(dir)
413 except (FileNotFoundError
, IsADirectoryError
, PermissionError
):
415 except NotADirectoryError
:
418 if first_no_init_file
is None:
419 if dir.joinpath('__init__.py').exists():
420 # In the case that a __init__.py exists, it's in 99% just a
421 # Python package and the project sits at least one level above.
423 elif not dir.is_file():
424 first_no_init_file
= dir
426 if _is_django_path(dir):
427 project
= Project(dir)
428 project
._django
= True
431 if probable_path
is None and _is_potential_project(dir):
434 if probable_path
is not None:
435 return Project(probable_path
)
437 if first_no_init_file
is not None:
438 return Project(first_no_init_file
)
440 curdir
= path
if path
.is_dir() else path
.parent
441 return Project(curdir
)
444 def _remove_imports(names
):
447 if n
.tree_name
is None or n
.api_type
not in ('module', 'namespace')